В чем разница между шардированием и репликацией?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
В чем разница между шардированием и репликацией?
Шардирование и репликация — это два разных подхода к масштабированию баз данных. Репликация повышает надёжность (копирует данные на несколько серверов), шардирование повышает пропускную способность (распределяет данные по серверам). Часто используются вместе.
Репликация: копирование данных
Репликация создает копии одних и тех же данных на разных серверах
┌─────────────────┐
│ 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 copy | Partition |
| Использование | Backup + read scaling | Write scaling |
| Consistency | Eventual (с 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 распределяют нагрузку
- Это даёт и надёжность, и масштабируемость