← Назад к вопросам

В чем разница между шардированием и репликацией?

2.8 Senior🔥 121 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

В чем разница между шардированием и репликацией?

Шардирование и репликация — это два разных подхода к масштабированию баз данных. Репликация повышает надёжность (копирует данные на несколько серверов), шардирование повышает пропускную способность (распределяет данные по серверам). Часто используются вместе.

Репликация: копирование данных

Репликация создает копии одних и тех же данных на разных серверах

┌─────────────────┐
│  Master (Primary)│
│  Users table    │
│  (read/write)   │
└────────┬────────┘
         │ replicate
    ┌────▼─────┬─────────┐
    │           │         │
┌───▼──┐  ┌──▼──┐  ┌──▼──┐
│Slave1│  │Slave2│  │Slave3│
│(read)│  │(read)│  │(read)│
└──────┘  └──────┘  └──────┘

Все данные ОДИНАКОВЫЕ на всех серверах
// Репликация: один источник истины (master)
public class ReplicationExample {
    public static void main(String[] args) {
        // Конфигурация
        // Master: localhost:3306
        // Slaves: localhost:3307, localhost:3308, localhost:3309
        
        // Писать только в master
        Connection masterConn = getConnection("master:3306");
        Statement stmt = masterConn.createStatement();
        stmt.execute("INSERT INTO users (name) VALUES ('John')");
        // Данные автоматически реплицируются на slaves
        
        // Читать из slaves (балансировка нагрузки)
        Connection slave1 = getConnection("slave1:3307");
        Connection slave2 = getConnection("slave2:3308");
        
        // Каждый запрос читает тот же данные
        ResultSet rs1 = slave1.createStatement().executeQuery("SELECT * FROM users");
        ResultSet rs2 = slave2.createStatement().executeQuery("SELECT * FROM users");
        // rs1 и rs2 содержат одинаковые данные
    }
}

Процесс репликации

Время    Master          Binlog                  Slave1              Slave2
────────────────────────────────────────────────────────────────────────
T0       INSERT user1    [event1]                (ждёт)
         ↓
T1       INSERT user2    [event2]    ←──────→   INSERT user1        (ждёт)
         ↓
T2       UPDATE user1    [event3]    ←──────→   INSERT user2        INSERT user1
                                                 ↓
T3       -                           ←──────→   UPDATE user1         INSERT user2
                                                 ↓
T4       -                           ←──────→   -                    UPDATE user1

Все данные одинаковые, но с задержкой (lag)

Шардирование: распределение данных

Шардирование разбивает данные на части, каждая часть на отдельном сервере

┌──────────────────────────┐
│    Router/Proxy          │
│  (выбирает правильный    │
│   shard по ключу)        │
└──────────┬───────────────┘
           │
     ┌─────┼─────┬─────────┐
     │     │     │         │
┌────▼──┐┌──▼──┐┌──▼──┐┌──▼──┐
│Shard0 ││Shard1││Shard2││Shard3│
│id%4=0 ││id%4=1││id%4=2││id%4=3│
│(1,5,9)││(2,6) ││(3,7) ││(4,8) │
└───────┘└──────┘└──────┘└──────┘

Разные данные на РАЗНЫХ серверах
// Шардирование: данные распределены
public class ShardingExample {
    private static final int SHARD_COUNT = 4;
    
    // Определяем какой shard использовать
    private int getShard(int userId) {
        return userId % SHARD_COUNT;  // hash-based sharding
    }
    
    // Вставка
    public void insertUser(int userId, String name) throws SQLException {
        int shardId = getShard(userId);
        
        // Подключаемся к правильному shard
        Connection conn = getConnection("shard" + shardId);
        
        String sql = "INSERT INTO users (id, name) VALUES (?, ?)";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.setInt(1, userId);
        pstmt.setString(2, name);
        pstmt.executeUpdate();
    }
    
    // Чтение
    public User getUser(int userId) throws SQLException {
        int shardId = getShard(userId);
        
        Connection conn = getConnection("shard" + shardId);
        String sql = "SELECT * FROM users WHERE id = ?";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.setInt(1, userId);
        
        ResultSet rs = pstmt.executeQuery();
        if (rs.next()) {
            return new User(rs.getInt("id"), rs.getString("name"));
        }
        return null;
    }
    
    // Поиск всех - ТРЕБУЕТ запроса ко ВСЕМ shards!
    public List<User> getAllUsers() throws SQLException {
        List<User> users = new ArrayList<>();
        
        for (int i = 0; i < SHARD_COUNT; i++) {
            Connection conn = getConnection("shard" + i);
            ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM users");
            
            while (rs.next()) {
                users.add(new User(rs.getInt("id"), rs.getString("name")));
            }
        }
        
        return users;  // собираем результаты из всех shards
    }
}

Сравнение

АспектРепликацияШардирование
ЦельНадёжность (HA)Масштабируемость (throughput)
РаспределениеВсе данные на каждом сервереРазные данные на разных серверах
ЕдиницаComplete copyPartition
ИспользованиеBackup + read scalingWrite scaling
ConsistencyEventual (с lag)Не applicable (разные данные)
Read queryК любому серверуК одному или нескольким shards
Write queryТолько в masterК одному shard
Резервная копияДа (slave = backup)Нет (потеря shard = потеря данных)
СложностьПростаяСложная
Hot spotВозможен в masterВозможен в shard

Типы репликации

1. Master-Slave репликация

Master (read/write) → Slave (read only)
Slave (read only) → Slave (read only)

Проблема: Master становится узким местом для write

2. Master-Master репликация

  Master1 ↔ Master2
 (read/write) (read/write)

Оба можно писать
Проблема: conflicts (конфликты при одновременных writes)

Типы шардирования

1. Range-based sharding

private int getShard(int userId) {
    if (userId < 1000) return 0;       // shard0: 0-999
    if (userId < 2000) return 1;       // shard1: 1000-1999
    if (userId < 3000) return 2;       // shard2: 2000-2999
    return 3;                          // shard3: 3000+
}

// Проблема: uneven distribution
// Если users 0-1000 активны, shard0 перегружена

2. Hash-based sharding

private int getShard(int userId) {
    return userId % SHARD_COUNT;  // даже распределение
}

// Проблема: rescaling сложный (нужно перехешировать все данные)

3. Directory-based sharding

private static Map<Integer, Integer> shardDirectory = new HashMap<>();

private int getShard(int userId) {
    return shardDirectory.get(userId);
}

// Преимущество: гибко, можно переместить данные
// Недостаток: нужна особая БД для mapping

Комбинирование: Replication + Sharding

На практике используются ВМЕСТЕ

┌─────────────────────────────────┐
│     Router/Load Balancer        │
└──────────────┬──────────────────┘
               │
     ┌─────────┼─────────┐
     │         │         │
 ┌───▼──┐ ┌───▼──┐ ┌────▼──┐
 │Shard0│ │Shard1│ │Shard2 │ (각 shard는 master)
 │Master│ │Master│ │Master │
 └───┬──┘ └───┬──┘ └────┬──┘
     │       │        │
 ┌───▼──┐ ┌──▼──┐ ┌──▼───┐
 │Shard0│ │Shard1│ │Shard2│ (각 shard의 slave)
 │Slave │ │Slave │ │Slave │
 └──────┘ └──────┘ └──────┘

Каждый shard имеет master-slave репликацию
Все shards вместе образуют полное распределение
@Configuration
public class ShardingWithReplication {
    @Bean
    public DataSource shard0Master() {
        return createDataSource("shard0-master:3306");
    }
    
    @Bean
    public DataSource shard0Slave() {
        return createDataSource("shard0-slave:3307");
    }
    
    @Bean
    public DataSource shard1Master() {
        return createDataSource("shard1-master:3308");
    }
    
    @Bean
    public DataSource shard1Slave() {
        return createDataSource("shard1-slave:3309");
    }
    
    // ... repeat для остальных shards
}

// Router logic
public class ShardingRouter {
    public DataSource getConnection(int userId, boolean isWrite) {
        int shardId = userId % SHARD_COUNT;
        
        if (isWrite) {
            return getShardMaster(shardId);  // всегда master для write
        } else {
            // round-robin между master и slave для read
            return getShardReadReplica(shardId);
        }
    }
}

Проблемы и решения

Репликация: лаг (replication lag)

// Problem: Read from slave может вернуть старые данные

Connection master = getMasterConnection();
Connection slave = getSlaveConnection();

// Write в master
master.createStatement().execute(
    "INSERT INTO users (id, name) VALUES (1, 'John')"
);

// Immediately read from slave - МОЖЕТ НЕ НАЙТИ!
ResultSet rs = slave.createStatement().executeQuery(
    "SELECT * FROM users WHERE id = 1"
);
if (!rs.next()) {
    System.out.println("User not found - slave lag!");
}

// Решение: После write читать из master
Connection master = getMasterConnection();
ResultSet rs = master.createStatement().executeQuery(
    "SELECT * FROM users WHERE id = 1"
);  // гарантированно найдем

Шардирование: join между shards

// Problem: JOIN между таблицами в разных shards

// SELECT u.*, o.* FROM users u
// JOIN orders o ON u.id = o.user_id
// WHERE u.id = 5

// user_id=5 в shard1, но order может быть в shard2!
// JOIN невозможен на БД уровне

// Решение: application-level join
public List<UserWithOrders> getUserOrders(int userId) {
    // 1. Получить пользователя из правильного shard
    int shardId = getShard(userId);
    User user = getUserFromShard(userId, shardId);
    
    // 2. Получить его заказы из того же shard
    List<Order> orders = getOrdersFromShard(userId, shardId);
    
    return new UserWithOrders(user, orders);
}

Шардирование: resharding (ресалт)

// Problem: нужно добавить новый shard
// Было: SHARD_COUNT = 4
// Нужно: SHARD_COUNT = 8 (добавить емкость)

// Старый hash: userId % 4
// Новый hash: userId % 8

// CADA пользователь должен быть перемещен в новый shard!
// Это очень дорогая операция

// Решение: consistent hashing
// Перемещаются только данные из соседних shards
// Не все данные

Когда использовать что

Репликация:

  • ✅ Нужна высокая доступность (HA)
  • ✅ Нужно распределить читаемую нагрузку
  • ✅ Нужен backup/failover
  • ✅ Данные умещаются на одном сервере

Шардирование:

  • ✅ Данные НЕ умещаются на одном сервере
  • ✅ Нужна линейная масштабируемость
  • ✅ Нужна распределённая запись
  • ✅ Работаем с очень большими объемами

Вывод

Репликация (1:N копирование):

  • Решает проблему надёжности и читаемой нагрузки
  • Один master, много slaves
  • Все данные везде одинаково
  • Используется для HA и read scaling

Шардирование (1:N распределение):

  • Решает проблему масштабируемости
  • Данные разбиты по shards
  • Каждый shard имеет часть данных
  • Используется для write scaling

Best Practice: Используй репликацию + шардирование вместе

  • Каждый shard реплицируется (master-slave)
  • Множество shards распределяют нагрузку
  • Это даёт и надёжность, и масштабируемость
В чем разница между шардированием и репликацией? | PrepBro