1. 完整回答
分库分表是将原来集中在一个数据库或一张表中的数据,按照业务或者一定的分片规则拆分到多个数据库、多个数据表中,主要解决单库单表数据量过大、读写压力过高以及存储容量不足的问题。
分库分表主要分为垂直拆分和水平拆分。
垂直拆分是按照业务或者字段进行拆分,比如把用户、订单、商品拆到不同的数据库,或者把一张表中不常访问的大字段拆到扩展表。
水平拆分是把同一张表的数据按照某个字段分散到不同的库表,比如订单表按照用户 ID 进行哈希分片,让不同用户的订单分布在不同的数据库中。
它的核心机制是分片键、分片算法和数据路由。
应用发起 SQL 后,分片中间件会解析查询条件,根据分片键计算目标库表,将 SQL 路由到对应的数据库执行。如果涉及多个分片,还需要对结果进行归并、排序或聚合。
常见的实现方案有 ShardingSphere-JDBC 和 ShardingSphere-Proxy。
分库分表最大的优势是能够分散数据存储和访问压力,实现数据库水平扩展。但同时也会引入跨库关联查询、分布式事务、全局唯一 ID、分页排序以及数据迁移等复杂问题。
实际项目中,通常会先优化索引、SQL、缓存和读写分离。当单库的容量或写入能力仍然存在明显瓶颈时,再考虑分库分表,比较适合订单、交易流水、日志等数据量持续增长的业务。
2. 核心原理与关键概念
2.1 核心原理
分库分表的核心是通过分片规则确定数据位置,并对 SQL 进行路由和执行。
整体流程:
SQL 解析 → 分片路由 → SQL 改写 → SQL 执行 → 结果归并
-
SQL 解析:解析 SQL 中的表名、查询条件和分片键。
-
分片路由:根据分片算法计算需要访问的数据库和数据表。
-
SQL 改写:将逻辑表名改成实际表名,必要时调整分页等 SQL 内容。
-
SQL 执行:在目标分片执行 SQL,多个分片可以并行执行。
-
结果归并:如果查询涉及多个分片,需要合并结果,并完成排序、聚合或分页。
例如,将订单数据按照用户 ID 分成 4 张表:
tableIndex = hash(userId) % 4
用户查询自己的订单时,根据用户 ID 就能定位到对应的订单表。
如果 SQL 没有分片键,也没有其他可用的路由信息,就可能需要访问所有分片,再合并结果。
因此,分片键能否覆盖主要查询场景,直接影响分库分表后的性能。
2.2 关键概念
| 概念 | 含义 |
|---|---|
| 分片键 | 决定数据存储位置的字段,例如用户 ID、订单 ID |
| 分片算法 | 计算数据应该进入哪个分片的规则,例如哈希、取模、范围分片 |
| 逻辑表 | 应用访问的统一表名,例如 t_order |
| 物理表 | 实际存储数据的表,例如 t_order_0、t_order_1 |
| 数据路由 | 根据分片规则定位需要访问的库表 |
| 绑定表 | 具有相同分片规则、可以实现分片内关联的表,例如订单表和订单明细表 |
| 广播表 | 在多个数据库中保存相同数据的小表,例如地区字典表 |
3. 高频面试追问
3.1 什么情况下需要分库分表?
当单库的存储容量、写入能力或者单表数据规模已经成为明显瓶颈,并且常规优化难以解决时,可以考虑分库分表。
主要看三个方面:
-
数据规模:单表数据持续增长,索引维护、查询、备份或数据清理成本明显增加。
-
访问压力:单库 CPU、磁盘 I/O、连接数或者写入吞吐量长期接近瓶颈。
-
扩展需求:业务还会持续增长,需要通过增加数据库节点分担压力。
实际判断时,应该先分析慢 SQL、执行计划和数据库监控指标,排除索引不合理、深分页、缓存使用不当等问题。
没有统一的分表行数阈值。即使单表达到千万级数据,只要性能和容量满足业务要求,也可以继续使用单表。
3.2 分片键应该怎么选择?常见的分片算法有哪些?
分片键优先选择查询频繁、分布均匀、取值稳定,并且能够让相关业务数据落在同一个分片的字段。
例如订单系统:
如果主要场景是用户查询自己的订单,可以选择用户 ID 作为分片键,让同一个用户的订单集中在一个分片,方便查询和分页。
常见分片算法有两种:
-
哈希分片:通过哈希和取模分配数据,通常分布比较均匀,适合按键查询,但范围查询和扩容比较麻烦。
-
范围分片:按照时间、ID 区间分配数据,方便范围查询和历史数据管理,但容易出现热点。
分片键的选择需要结合业务查询方式。比如按用户 ID 分片后,仅凭订单 ID 查询订单,就需要额外的路由机制。
3.3 分库分表后,查询条件没有分片键怎么办?
没有分片键时,可以通过路由映射表、辅助索引或者冗余查询数据解决,尽量避免全分片扫描。
例如订单按照用户 ID 分片,但支付回调只有订单 ID。
可以维护一张 order_id → user_id 的映射表,先通过订单 ID 找到用户 ID,再根据用户 ID 路由到对应分片。
另一种方式是在生成订单 ID 时编码路由信息,查询时直接解析目标分片。
对于复杂搜索,可以将数据同步到 Elasticsearch 等检索系统。
如果没有可用的路由信息,通常就需要访问多个甚至全部分片。随着分片数量增加,查询成本会明显上升。
3.4 分库分表后,跨库 JOIN 怎么解决?
最优先的方案是让经常关联的数据落在同一个分片,其次再考虑业务层关联或数据冗余。
比如订单表和订单明细表,可以采用相同的分片键和分片规则,让同一订单的数据存储在相同数据库中,这样仍然可以使用普通 JOIN。
如果数据已经分散到不同数据库,可以分别查询,再在应用层组装结果。
对于高频关联查询,还可以建立冗余字段、宽表或者专门的查询模型。
ShardingSphere 的绑定表能够优化符合分片规则的关联查询,但需要满足相应的分片条件。
跨分片 JOIN 通常会增加网络传输和结果归并成本,因此表之间的关联关系最好在分片设计阶段就考虑清楚。
3.5 分库分表后,如何保证主键全局唯一?
通常通过分布式 ID 生成方案保证全局唯一,常见方案包括 Snowflake 算法和数据库号段模式。
Snowflake 算法一般由时间戳、机器标识和序列号组成,可以在不同节点上独立生成 ID,吞吐量比较高,但需要处理机器标识冲突和时钟回拨问题。
号段模式则是从数据库批量申请一段 ID,在应用本地生成后续 ID,可以减少频繁访问数据库的开销。
数据库自增主键只能直接保证对应表内的唯一性,多库分别自增可能产生重复值。
另外,全局唯一 ID 和全局唯一业务字段是两个问题。
继续追问:分库分表后,如何保证手机号全局唯一?
如果手机号分散在不同分片,单个数据库的唯一索引只能约束本分片。
可以建立以手机号为唯一键的全局索引表,在注册时进行原子占用,并结合事务或者补偿机制维护一致性。也可以根据业务设计,让相同手机号始终路由到同一个分片进行唯一性校验。
3.6 分库分表后,分页、排序和聚合怎么处理?
单分片查询可以正常使用数据库的分页和排序;跨分片查询需要分别执行,再统一归并结果。
例如查询所有订单,按照创建时间排序并获取第 1001~1010 条记录。
如果数据分布在多个分片,可能需要各分片分别返回前 1010 条候选记录,然后在中间件统一排序,截取最终结果。
分片越多、分页越深,读取和归并的成本通常越高。
常见优化方式包括:
-
优先利用分片键,把查询限制在单个分片。
-
使用游标分页,通过创建时间和订单 ID 等稳定排序字段定位下一页。
-
对复杂统计、全局排序和报表查询建立专门的查询模型或分析系统。
COUNT、SUM 等聚合可以对分片结果进行合并,AVG 通常需要根据各分片的 SUM 和 COUNT 计算。
3.7 分库分表后,分布式事务怎么解决?
优先通过业务设计让事务落在单个数据库中;必须跨库时,再根据一致性要求选择分布式事务方案。
例如订单主表和明细表采用相同的分片规则,让同一笔订单的数据落在一个数据库中,就可以使用本地事务。
如果事务跨越多个数据库,常见方案有:
-
XA 两阶段提交:通过事务协调器统一提交或回滚多个数据库事务,适合一致性要求较高的场景,但会增加延迟和资源占用。
-
可靠消息与最终一致性:通过本地消息表、事务消息、重试和幂等处理,使不同服务或数据库最终达到一致状态。
-
TCC、Saga:通过业务预留、确认、取消或补偿机制协调多个操作,适合业务流程较复杂的场景。
例如订单创建和积分发放,可以通过可靠消息实现最终一致性,并提供失败重试和幂等保障。
具体选择取决于业务能否接受短暂不一致,以及对延迟、可用性和开发复杂度的要求。
3.8 分库分表后,如何扩容?怎样不停机迁移数据?
扩容的核心是调整数据路由,并安全地把受影响的数据迁移到新分片。
如果直接使用 hash(key) % N,分片数量发生变化时,大量数据的路由结果也会改变,因此直接修改取模数通常需要迁移大量数据。
一种常见方案是提前划分较多逻辑分片,再建立逻辑分片到物理数据库的映射。扩容时只迁移部分逻辑分片,并调整映射关系。
不停机迁移通常按照以下流程:
建立新库表 → 全量数据迁移 → 增量数据同步 → 数据一致性校验 → 灰度切换路由 → 观察并清理旧数据
增量同步可以利用 MySQL Binlog 或数据库迁移工具实现。
切换阶段需要保证写入路由正确,避免重复写入、数据遗漏以及新旧路由读写不一致,同时准备回滚方案。
3.9 分库分表后,出现数据倾斜或热点怎么办?
首先区分数据量倾斜和访问热点,再根据原因调整分片策略。
数据量倾斜指某些分片的数据远多于其他分片。常见原因是分片键分布不均匀,比如按照地区分片,但不同地区的用户数量差异很大。
访问热点指少数分片承受大量请求。例如按照商家 ID 分片,大商家的订单可能集中在一个分片。
常见处理方式有:
-
使用分布更均匀的哈希分片策略。
-
对热点读请求使用缓存,降低数据库访问压力。
-
将大租户或热点业务单独拆分。
-
对超大租户增加更细粒度的分片维度。
需要注意,普通哈希可以改善不同分片键之间的数据分布,但如果大量请求都集中在同一个分片键上,单纯增加分片数量通常无法消除热点。
4. 易混淆概念对比
4.1 水平拆分和垂直拆分
| 对比维度 | 水平拆分 | 垂直拆分 |
|---|---|---|
| 拆分依据 | 按数据行拆分 | 按业务或字段拆分 |
| 数据结构 | 各分片表结构基本相同 | 不同表或库的数据结构可以不同 |
| 主要作用 | 分散数据量和访问压力 | 降低业务耦合,拆分宽表 |
| 典型例子 | 订单按用户 ID 拆分 | 用户库、订单库独立部署 |
4.2 分库和分表
| 对比维度 | 分库 | 分表 |
|---|---|---|
| 拆分对象 | 数据库 | 数据表 |
| 主要作用 | 分散数据库实例的存储和负载 | 控制单表数据规模 |
| 扩展能力 | 部署在不同实例时可以水平扩展 | 同一实例内分表仍受实例资源限制 |
| 主要代价 | 跨库事务、运维复杂度 | 跨表查询、路由和结果归并 |
实际场景中,水平分库和水平分表经常组合使用。
4.3 哈希分片和范围分片
| 对比维度 | 哈希分片 | 范围分片 |
|---|---|---|
| 分片依据 | 哈希值或取模结果 | 时间、ID 等范围 |
| 数据分布 | 通常比较均匀 | 可能存在热点 |
| 等值查询 | 容易定位分片 | 容易定位分片 |
| 范围查询 | 可能涉及多个分片 | 可以按范围缩小查询范围 |
| 扩容特点 | 直接改变取模数可能引发大量迁移 | 可增加或调整范围,但可能需要迁移已有区间 |
4.4 分库分表和数据库分区
| 对比维度 | 分库分表 | 数据库分区 |
|---|---|---|
| 实现方式 | 业务或中间件管理数据分片 | 数据库内部管理表分区 |
| 数据位置 | 可以分布在不同数据库实例 | 普通 MySQL InnoDB 分区属于同一数据库实例 |
| 扩展能力 | 可以分散实例的存储和处理压力 | 主要优化数据组织、分区裁剪和维护 |
| 查询处理 | 可能需要中间件进行路由和归并 | 数据库优化器负责分区访问 |
数据库分区适合时间范围查询、历史数据清理等场景;跨实例分库分表更适合单机容量或负载已经成为瓶颈的场景。
4.5 分库分表和读写分离
| 对比维度 | 分库分表 | 读写分离 |
|---|---|---|
| 核心方式 | 将不同数据分散到不同库表 | 将读请求分配给只读副本 |
| 主要解决 | 数据规模、存储和读写扩展 | 读请求压力 |
| 写入能力 | 跨实例分片可以分散写入压力 | 写请求通常仍由主库处理 |
| 主要问题 | 分布式事务、跨分片查询、扩容 | 主从复制延迟、数据一致性 |
两种方案可以结合使用,例如每个分库再部署自己的主从副本。
5. 总结
分库分表主要用于解决单库单表的数据规模、存储容量和访问压力瓶颈。 它可以按照业务或字段进行垂直拆分,也可以通过分片键和分片算法进行水平拆分。核心流程是解析 SQL、计算路由、执行查询,再对跨分片结果进行归并。它能够分散数据和访问压力,并支持通过增加数据库实例实现水平扩展,但也会带来跨库事务、关联查询、分页排序和数据迁移等问题。设计时最关键的是选择合适的分片键,让主要查询和事务尽可能落在单个分片,同时提前考虑数据分布和扩容方案。实际项目中,通常会先完成索引、SQL 和读写架构优化,再根据真实容量和性能瓶颈决定是否引入分库分表。