本文目录导读:
这是一个非常经典且重要的问题,分库分表是应对数据库单库单表数据量过大、并发量过高时的一种常用解决方案。
我会从核心概念、常见策略、关键问题和实战建议四个方面来详细拆解。
核心概念:为什么要分?
在深入策略前,先明确几个核心概念:
- 分库:将数据分散存储到多个数据库实例中。
- 目的:突破单台服务器的IO/CPU/内存瓶颈,解决高并发写入和读取的压力。
- 效果:提升读写性能(并行处理),提高数据库可用性(单库故障不影响整体)。
- 分表:将一张大表的数据拆分到多个相同结构的小表中。
- 目的:单表数据量过大导致的索引失效、DDL(数据定义语言,如修改表结构)锁表、查询性能下降等问题。
- 效果:缩小单表数据量,提升查询速度(尤其是范围查询和排序),减少索引层数。
我们说的“分库分表”即 “分库” 或 “分表” ,或两者结合使用,即 “分库 + 分表”。
常见分库分表策略
核心策略只有一种:选择一个或多个字段作为分片键(Sharding Key),按照一定的规则将数据路由到不同的库/表中。
常见的分片策略有以下几种:
基于范围(Range)分片
- 原理:按某个字段的值范围(如ID范围、时间范围)分片。
- 用户ID 1-1000万 在
db0,1001万-2000万 在db1;或者按月份分表order_202401,order_202402。
- 用户ID 1-1000万 在
- 优点:
- 扩容简单:只需增加新的库/表,无需迁移历史数据。
- 范围查询友好:可以快速定位到可能存在的库/表(但可能还需遍历多个库)。
- 缺点:
- 数据热点:新数据集中在最新库/表(如当月表),旧数据极少访问,导致访问不均衡。
- 分片键局限性:如果分片键不是查询条件,需要广播查询所有库/表(性能灾难)。
基于哈希(Hash)分片
- 原理:对分片键计算哈希值,然后取模(
hash(key) % N)得到目标库/表序号。user_id % 4 = 0->db0,=1->db1。
- 优点:
- 数据分布均匀:能有效避免热点问题。
- 缺点:
- 扩容灾难:如果从
N扩容到N+1,hash(key) % N变为hash(key) % (N+1),绝大部分数据需要重新映射和迁移(俗称“惊群效应”)。 - 范围查询极差:无法直接进行范围查询,因为数据散落在各个分片。
- 扩容灾难:如果从
一致性哈希(Consistent Hash)分片
- 原理:为了解决哈希分片扩容时的数据迁移问题,在哈希环上,将数据和节点都映射到环上,每个数据映射到其顺时针方向的第一个节点。
- 优点:
- 平滑扩容:增加或减少节点时,只影响该节点在环上相邻的节点的数据,迁移量小(约
1/N的数据量)。 - 负载均衡:可通过引入虚拟节点进一步均衡。
- 平滑扩容:增加或减少节点时,只影响该节点在环上相邻的节点的数据,迁移量小(约
- 缺点:
- 范围查询依然不友好。
- 实现复杂度较高(通常由中间件如 Redis Cluster、Vitess 或自研组件实现)。
混合策略(Range + Hash / 多级分片)
这是实际生产中最常用的策略,用于解决单一策略的缺陷。
- 先Range后Hash
- 思路:先按时间范围分到不同的“库组”(如
group_2024),再在每个组内按user_id哈希分表(如table_0到table_15)。 - 优点:兼顾了时效性(冷热数据分离)和数据均匀,扩容时,只需要新建库组,数据迁移量小。
- 思路:先按时间范围分到不同的“库组”(如
- 自定义分片规则
- 思路:不依赖严格数学计算,而是根据业务特征人工制定复杂映射表(路由表)。
地区码->数据库,用户ID->表。 - 优点:极高灵活性,能处理非常复杂的业务场景。
- 缺点:需要维护一个中心化的路由配置,可能成为性能瓶颈或单点故障。
- 思路:不依赖严格数学计算,而是根据业务特征人工制定复杂映射表(路由表)。
必须解决的关键问题
分库分表带来的收益是巨大的,但引入的复杂性也同样巨大,以下是必须提前规划好的核心问题:
全局主键生成
- 问题:自增主键在分库分表下会重复。
- 方案:
- 雪花算法(Snowflake):业界最常用的ID生成算法,生成64位Long整数,包含时间戳、机器ID、序列号,全局唯一、趋势递增。
- 数据库号段模式:从数据库批量获取ID号段(如每次取1000个ID),应用层内存分配,性能优异。
- Redis/Lua:利用Redis的原子性INCR命令生成。
- UUID:最简单,但太长、无序,不适合做主键索引。
跨分片查询
- 问题:SQL中不包含分片键,不得不查询所有分片。
- 方案:
- 避免:在业务设计上,强制查询必须携带分片键(如
WHERE user_id = ?),这是最根本的解决方案。 - 路由表/索引表:建立一个额外的“索引”表,将非分片键与分片键的对应关系存储起来,通过索引表查到分片键,再路由到具体分片。
SELECT db_index FROM idx_user_name WHERE user_name = 'xxx'。 - 中间件聚合:使用 ShardingSphere、MyCAT 等中间件,自动将SQL广播到所有分片,然后聚合结果。性能较差,仅适合低频、非关键查询。
- 避免:在业务设计上,强制查询必须携带分片键(如
跨分片事务
- 问题:一次操作需要更新多个分片的数据(如转账,扣款和加款在不同库)。
- 方案:
- 分布式事务:
- 强一致性(XA/JTA):性能差,基本不用。
- 最终一致性(TCC、Saga、本地消息表):主流方案,如使用 Seata 框架。
- 业务规避:
- 尽量按业务领域拆分,让一个核心事务只在一个分片中完成(把交易记录和用户余额放在同一个库的同表或不同表里,通过
user_id分片)。 - 使用 柔性事务,接受短暂的不一致。
- 尽量按业务领域拆分,让一个核心事务只在一个分片中完成(把交易记录和用户余额放在同一个库的同表或不同表里,通过
- 分布式事务:
分页与排序
- 问题:
ORDER BY ... LIMIT 10, 10在各分片执行后,无法直接得到全局第11-20名的数据。 - 方案:
- 全局排序:中间件从所有分片拉取(比如第1-20名)的所有数据(如
limit 0, 20),然后排序后取全局第11-20名。数据量越大,性能越差。 - 业务规避:
- 禁止深层分页:允许用户翻到第100页的场景极罕见,可以限制翻页深度(如最多100页)。
- 游标分页:使用
WHERE id > last_id LIMIT 10的方式代替LIMIT 0,10,这在分片场景下可以高效实现,因为每个分片都可以独立返回id > last_id的前10条,然后由中间件合并排序。这是推荐方案。
- 全局排序:中间件从所有分片拉取(比如第1-20名)的所有数据(如
数据迁移与扩容
- 问题:如何从单库单表迁移到分库分表?如何从4个库扩容到8个库?
- 方案:
- 双写迁移:旧表正常服务,同时写一份到新表,然后对旧数据做一致性校验和回填。
- 平滑扩容:使用一致性哈希或中间件(如 ShardingSphere-Proxy)支持动态修改分片规则,逐步迁移数据。
- 停机迁移:在所有方案中,最安全(但影响业务)。
实战建议:先别急着分!
-
能不分就不分
- 98% 的场景不需要分库分表,先尝试数据库优化:索引优化、SQL优化、读写分离、缓存(Redis)、垂直拆分(大字段分离)。
- 分库分表是最后手段,引入的复杂度远超收益。
-
提前规划规则
- 分片键、分片算法、扩容策略,必须在设计之初就确定,事后修改成本极高。
- 建议使用业界成熟的中间件(如 Apache ShardingSphere、MyCAT、Vitess),而不是自己开发轮子。
-
从单库单表开始,留好扩展接口
- 代码层面:数据源、DAO层设计为可插拔,比如通过
@DataSource注解或动态路由,方便切换。 - 数据库层面:不要使用自增主键,改用分布式ID生成器。
- 先使用单库分表(只在同一数据库里分表),等单库也撑不住了,再升级到分库+分表。
- 代码层面:数据源、DAO层设计为可插拔,比如通过
总结表格
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 范围分片 | 扩容简单、范围查询好 | 数据热点、分片键局限 | 日志数据、时序数据(按时间切分) |
| 哈希分片 | 数据均匀、无热点 | 扩容灾难、范围查询差 | 用户数据、订单数据(按用户ID/订单ID切分) |
| 一致性哈希 | 平滑扩容 | 实现复杂、范围查询差 | 需要频繁扩容的场景、分布式存储系统 |
| 混合策略 | 兼具平衡与可扩展 | 设计复杂 | 绝大多数企业级高并发、高数据量系统 |
最后记住:分库分表不是技术升级,而是架构妥协,如果业务允许,优先考虑 NoSQL(如 MongoDB、Cassandra)或分布式数据库(如 TiDB),它们原生支持自动分片和扩容,能省去大量运维和开发成本。
标签: 数据分片