聊到架构,分库分表是必答题。常见问法:什么时候需要分库分表?分片键怎么选?分完之后跨库查询怎么办?
今天把这几件事讲透。
一、先别急着分:能不分就不分
分库分表是最后手段,因为它会带来一大堆新问题。在此之前,有几件事更便宜:
- 加索引、优化慢 SQL
- 读写分离
- 冷热分离 / 归档
- 上缓存。热点数据挡在 Redis 前面,数据库压力能降一个量级。
判断信号也很明确:单表数据量过大导致查询明显变慢,或者单库的写入/QPS 顶到硬件上限,这时候再考虑分。
二、两种拆法:垂直和水平
垂直拆分是按业务或字段拆:
- 垂直分库:按业务域拆成用户库、订单库、商品库,互不干扰。
- 垂直分表:把大字段(text、blob)或低频字段拆到扩展表,主表更“瘦”,查询更快。
水平拆分是按数据行拆:
- 水平分表:同一个库内,把一张表按规则拆成 user_0、user_1……
实践中通常是先垂直(按业务拆库)→ 再水平(大表拆小表)。
三、分片键怎么选,这是最关键的一步
分片键决定了数据落在哪个分片,选错了后面全是麻烦。三条经验:
1. 高基数、分布均匀。用性别、状态这种只有几个值的字段,数据会全挤在一个分片里。
2. 大多数查询都能带上它。
分片键最好就是业务查询的必经条件,比如订单按 user_id 分,那“查我的订单”就只打一个分片。
3. 尽量避免跨分片。如果按 user_id 分片,却经常要按 order_id 查,那每次都得扫全部分片。
常见的替代方案是基因法:把分片键的信息“嵌入”到另一个 ID 里,让两个维度都能定位到同一个分片。
// 水平分表路由:按 user_id 取模定位表
int idx = Math.abs(userId.hashCode()) % 64;
String table = "orders_" + idx;
String sql = "select * from " + table + " where user_id = ?";
四、分片算法
- 范围分片:按 ID 或时间区间分。扩容方便,但容易热点(比如按时间分,写入全压在新分片上)。
- 哈希取模:
hash(key) % 分片数。分布均匀,但扩容时要重新分布数据,非常痛。 - 一致性哈希:减少扩容时的数据迁移量,配合虚拟节点解决分布不均。
所以很多团队宁可预留足够多的分片(比如一开始就分 64 张表),避免后期扩容。
五、分完之后必须面对的五个麻烦
1. 跨库 JOIN。分片后 JOIN 基本不可用,只能在应用层组装,或者做冗余字段。
2. 跨库事务。本地事务管不了多个库,需要分布式事务或最终一致方案。
3. 分页与排序。limit 100, 10 这种要各个分片都查一遍再归并,越翻到后面越慢。
4. 全局唯一 ID。自增主键不再全局唯一,得换成雪花算法、号段模式或者 Redis 自增。
5. 扩容与数据迁移。怎么平滑地把数据搬到新分片,是个大工程。
六、中间件还是自己写
- 客户端分片:应用里自己做路由(ShardingSphere-JDBC 这类)。少了网络一跳,但和代码耦合。
- 代理层分片:ShardingSphere-Proxy、MyCat 这类独立服务。应用无感,但多一层转发和运维成本。
选哪个看团队:要简单可控就自己写,要透明通用就上中间件。
一句话记忆
1. 分库分表是最后手段,先试索引优化、读写分离、冷热分离、缓存。
2. 先垂直拆业务,再水平拆大表。
3. 分片键要高基数、分布均匀、且是查询必经条件。
4. 范围分片易热点,取模分布均匀但扩容痛,一致性哈希折中。
5. 分完要面对:跨库 JOIN、跨库事务、分页排序、全局 ID、数据迁移。
分库分表难的不是“怎么分”,而是“分完之后的那堆问题你打算怎么接”。面试时能主动说出这些坑,比只会背拆法的人靠谱得多。