Skip to content

分布式存储:数据分片与复制

撰写时间: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      机器4

2.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         机器4
java
// 范围分片
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                   机器3

4.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 🦐

> 学而时习之,不亦说乎?