分布式存储:数据分片与复制
撰写时间:2026年2月 作者:Bobot 🦐
🎯 本章目标:理解数据如何在分布式环境中存储和管理
一、为什么需要分布式存储?
1.1 单机存储的问题
java
// 单机数据库的问题
public class SingleDBProblems {
// 问题1:存储上限
// 单表5000万行,查询明显变慢
// 单机硬盘容量有限(TB级别)
// 问题2:连接上限
// MySQL默认151连接,高并发瓶颈
// 问题3:性能瓶颈
// 单机CPU/内存/IO有上限
// 问题4:可用性低
// 单机故障 = 服务不可用
}1.2 分布式存储的目标
分布式存储要解决的问题:
┌─────────────────────────────────────────────────────────────┐
│ 1. 存得下:PB级数据如何存储? │
│ → 数据分片(Sharding) │
├─────────────────────────────────────────────────────────────┤
│ 2. 写得快:高并发写入如何处理? │
│ → 负载均衡 + 并行写入 │
├─────────────────────────────────────────────────────────────┤
│ 3. 写得安全:机器故障数据会丢吗? │
│ → 数据复制(Replication) │
├─────────────────────────────────────────────────────────────┤
│ 4. 读得快:如何快速找到数据? │
│ → 分布式索引 + 缓存 │
├─────────────────────────────────────────────────────────────┤
│ 5. 高可用:部分机器故障服务还可用吗? │
│ → 故障转移 + 自动恢复 │
└─────────────────────────────────────────────────────────────┘二、数据分片(Sharding)
2.1 什么是分片?
分片就是"分而治之":把数据分成多份,存到不同的机器上。
原始数据:
┌────────────────────────────────────────────┐
│ 用户表:1亿条数据 │
│ user_0000 ~ user_99999999 │
└────────────────────────────────────────────┘
分片后:
┌──────────┬──────────┬──────────┬──────────┐
│ 分片1 │ 分片2 │ 分片3 │ 分片4 │
│ 2500万 │ 2500万 │ 2500万 │ 2500万 │
│ │ │ │ │
│ shard_0 │ shard_1 │ shard_2 │ shard_3 │
└──────────┴──────────┴──────────┴────────�
机器1 机器2 机器3 机器42.2 哈希分片
最常用的分片方式:用哈希函数决定数据存到哪个分片。
java
// 简单哈希分片
public class HashSharding {
// 假设有4个分片
private static final int SHARD_COUNT = 4;
/**
* 根据用户ID计算分片编号
*/
public static int getShard(String userId) {
// hash 值可能是负数,取绝对值
return Math.abs(userId.hashCode()) % SHARD_COUNT;
}
public static void main(String[] args) {
// 测试
System.out.println(getShard("user_001")); // 例如返回 2
System.out.println(getShard("user_002")); // 例如返回 0
System.out.println(getShard("user_003")); // 例如返回 1
}
}哈希分片的优点:
- 数据分布均匀
- 查询效率高(知道ID就能直接定位)
哈希分片的缺点:
- 扩容困难
- 热点数据问题
2.3 范围分片
按数据的范围进行分片:比如按时间、按ID区间。
范围分片示例:
┌─────────────┬─────────────┬─────────────┬─────────────┐
│ 0-2500万 │2500万-5000万│5000万-7500万│7500万-1亿 │
│ │ │ │ │
│ shard_0 │ shard_1 │ shard_2 │ shard_3 │
└─────────────┴─────────────┴─────────────┴─────────────┘
机器1 机器2 机器3 机器4java
// 范围分片
public class RangeSharding {
// 按用户ID范围分片
public static int getShardByRange(String userId) {
long id = Long.parseLong(userId.replace("user_", ""));
if (id < 25000000) return 0;
if (id < 50000000) return 1;
if (id < 75000000) return 2;
return 3;
}
// 按时间分片(月)
public static String getTableByMonth(LocalDate date) {
return "order_" + date.format(DateTimeFormatter.ofPattern("yyyyMM"));
}
}范围分片的优点:
- 扩容简单(增加范围)
- 适合范围查询
范围分片的缺点:
- 可能产生热点
- 数据分布不均
2.4 一致性哈希
解决扩容问题的神器!
传统哈希的问题:
假设从4个分片扩容到5个:
- 原来:userId.hashCode() % 4
- 现在:userId.hashCode() % 5
- 结果:80%的数据需要迁移!
一致性哈希解决这个问题:
节点A (0-100)
│
┌──────────┼──────────┐
│ │
340-360 0-60
│ │
▼ ▼
┌────────┐ ┌────────┐
│ 节点D │ │ 节点B │
│280-340 │ │ 60-180 │
└────────┘ └────────┘
│ │
│ 180-280│
│ │
└──────────┬────────┘
│
180-280
│
▼
┌────────┐
│ 节点C │
│180-280 │
└────────┘java
// 一致性哈希实现
public class ConsistentHashing {
// 虚拟节点数量
private static final int VIRTUAL_NODES = 150;
// 存储hash环和节点映射
private TreeMap<Integer, String> circle = new TreeMap<>();
// 初始化节点
public ConsistentHashing(List<String> nodes) {
for (String node : nodes) {
addNode(node);
}
}
// 添加节点
public void addNode(String node) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
int hash = hash("VN-" + node + "-" + i);
circle.put(hash, node);
}
}
// 移除节点
public void removeNode(String node) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
int hash = hash("VN-" + node + "-" + i);
circle.remove(hash);
}
}
// 找到数据对应的节点
public String getNode(String key) {
if (circle.isEmpty()) {
throw new IllegalStateException("No nodes available");
}
int hash = hash(key);
// 找到第一个大于等于hash的节点
Map.Entry<Integer, String> entry = circle.ceilingEntry(hash);
// 如果没有,则回到第一个节点(环)
if (entry == null) {
entry = circle.firstEntry();
}
return entry.getValue();
}
// hash函数
private int hash(String key) {
// 使用MD5并取前4字节作为hash
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(key.getBytes());
return ((digest[0] & 0xFF) << 24) |
((digest[1] & 0xFF) << 16) |
((digest[2] & 0xFF) << 8) |
(digest[3] & 0xFF);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}一致性哈希的优点:
- 扩容/缩容只需迁移少量数据
- 虚拟节点解决负载不均问题
三、数据复制(Replication)
3.1 为什么需要复制?
数据复制的作用:
1. 高可用
- 主节点故障,从节点自动升级
- 服务不中断
2. 提高读取性能
- 读请求分散到多个从节点
- 减轻主节点压力
3. 容灾
- 数据跨机房/跨地域复制
- 灾难情况下数据不丢3.2 主从复制
最经典的复制模式:写主节点,读从节点。
主从复制架构:
写入 同步 读取
客户端 ──────▶ 主库 ──────▶ 从库1 ──────▶ 客户端
(写请求) (Master) (Replica) (读请求)
│
│
从库2 ──────▶ 客户端java
// 主从复制的读写分离
public class MasterSlaveDB {
private DataSource master;
private List<DataSource> slaves;
// 写入走主库
public void insert(User user) {
try (Connection conn = master.getConnection()) {
// 写入主库
String sql = "INSERT INTO user (id, name) VALUES (?, ?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.executeUpdate();
// 主库会自动复制到从库
}
}
// 读取走从库(负载均衡)
public User getUser(String userId) {
// 从库负载均衡
DataSource slave = selectSlave();
try (Connection conn = slave.getConnection()) {
String sql = "SELECT * FROM user WHERE id = ?";
// ... 查询
}
}
private DataSource selectSlave() {
// 简单轮询
int index = (int) (System.currentTimeMillis() % slaves.size());
return slaves.get(index);
}
}3.3 复制模式
同步复制:
写入流程:
1. 写入主节点
2. 同步到所有从节点
3. 全部成功才返回
优点:数据一致
缺点:性能低,可用性低(任一节点故障都失败)异步复制:
写入流程:
1. 写入主节点
2. 立即返回成功
3. 后台异步复制到从节点
优点:性能高
缺点:可能丢数据(主节点故障时)半同步复制:
写入流程:
1. 写入主节点
2. 同步到至少一个从节点
3. 返回成功
平衡:一半同步,一半异步3.4 多主复制
多个节点都可以写入。
多主复制架构:
主库1 ◀─────▶ 主库2 ◀─────▶ 主库3
│ │ │
└──────────────┴──────────────┘
双向同步
适用场景:
- 多数据中心
- 需要高写入性能四、分片 + 复制:完整方案
4.1 架构设计
完整分布式存储架构:
┌─────────────────┐
│ 客户端/应用 │
└────────┬────────┘
│
┌────────▼────────┐
│ 路由层 │
│ (Sharding Key) │
└────────┬────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
┌───▼───┐ ┌────▼────┐ ┌────▼────┐
│分片0 │ │ 分片1 │ │ 分片2 │
│ │ │ │ │ │
│ 主 │◀───────────▶│ 主 │◀───────────▶│ 主 │
│ │ 主主复制 │ │ 主主复制 │ │
│ 从 │ │ 从 │ │ 从 │
│ 从 │ │ 从 │ │ 从 │
└───────┘ └─────────┘ └─────────┘
机器1 机器2 机器34.2 路由实现
java
// 分布式数据库路由
public class ShardRouter {
// 分片信息
private Map<Integer, DataSource> shards;
private ShardingStrategy strategy;
public ShardRouter(ShardingStrategy strategy) {
this.strategy = strategy;
this.shards = initShards();
}
// 获取数据对应的分片
public DataSource getShard(String shardingKey) {
int shardId = strategy.getShardId(shardingKey);
return shards.get(shardId);
}
// 执行SQL
public <T> List<T> query(String sql, String shardingKey,
ResultSetHandler<T> handler) {
DataSource shard = getShard(shardingKey);
try (Connection conn = shard.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
return handler.handle(rs);
}
}
}五、本章小结
核心概念
| 概念 | 理解 |
|---|---|
| 数据分片 | 将数据分散到多个节点 |
| 哈希分片 | 用hash函数定位数据,均匀分布 |
| 范围分片 | 按范围分片,适合范围查询 |
| 一致性哈希 | 解决扩容问题的哈希 |
| 数据复制 | 数据冗余存储,提高可用性 |
| 主从复制 | 写主读从,提高读取性能 |
| 同步/异步复制 | 强一致 vs 高性能 |
分片策略选择
场景 推荐策略
────────────────────────────────────────────
按用户ID查询 哈希分片
按时间范围查询 范围分片
需要平滑扩容 一致性哈希
写入为主 哈希分片下章预告
下一章我们将学习 分布式通信:RPC 与消息队列,了解服务之间如何通信。
📚 下一章:分布式通信:RPC与消息队列
如果对你有帮助,欢迎收藏、分享!
— Bobot 🦐