我们订单库是去年做的分库分表,当时被数据量逼的------订单表两个月涨了3000万行,单表查询开始变慢,领导拍板拆。
一年过去了,系统没出大事故,但回头看,有三个当时没想清楚的问题,后来都变成了持续的阵痛。写出来,给准备分库分表的兄弟一个参考。
先声明:这篇不是劝你别分库分表,而是劝你想清楚再动。分库分表是不可逆的大手术,动手之前把该想的想透,能省掉后面一年的头疼。
第一件没想清楚的事:分片键选得对不对
当时选分片键,几乎是拍脑袋定的:订单表按订单ID取模分16库32表。理由很简单------订单ID查询最多。
这个选择在90%的场景下是对的,但剩下的10%让我们难受了一整年。
订单业务最大的特点是:查询通常按用户维度来,而不是按订单维度。 用户查"我的订单",天然是按用户ID查的。我们却用订单ID做了分片键,导致"按用户查订单"这个最高频的查询,变成了一次全库扫描------16个库全部查一遍,再合并结果。
sql
-- 这条SQL在分片键为订单ID的架构下,会打到所有16个库
SELECT * FROM t_order WHERE user_id = 12345 ORDER BY create_time DESC LIMIT 20;
正确的做法,我们后来才意识到:分片键的选择要跟着最高频的查询走,而不是跟着主键走。
当时如果选了用户ID做分片键:
- "查某个用户的订单"------直接定位到一个分片,一次查询搞定
- "查某个订单的详情"------需要维护一个订单ID到用户ID的映射关系,或者冗余一份用户ID在订单表里
两种方案各有取舍,但"用户查订单"是最高频场景,应该优先保它。
复盘结论:分片键 = 最高频查询的维度。 别被"主键查询快"这个惯性思维带偏。
第二件没想清楚的事:跨分片查询和聚合,代价比想象中大
分库分表之后,很多"以前一条SQL搞定"的查询,变成了麻烦事。
最典型的是分页排序。 以前:
sql
SELECT * FROM t_order ORDER BY create_time DESC LIMIT 0, 20;
现在这条SQL要在16个库各查20条,再在应用层合并排序,取前20。数据量越大、页数越深,这个操作的代价越高。 深分页(翻到第100页)基本是灾难,16个库各查2000条,应用层排序合并,延迟直接上秒。
我们的解决方案是:
- 列表页放弃深分页,改成游标分页(基于上次的ID和创建时间),既快又稳
- 统计类查询全部走离线,用定时任务预聚合,或者上数据仓库,绝不实时查分片
java
// 游标分页:传入上次的最后一条记录ID,比LIMIT OFFSET高效得多
public List<Order> pageByCursor(Long lastId, LocalDateTime lastTime, int size) {
// 先根据lastId定位到分片
int shard = shardOf(lastId);
// 只在对应的分片上查询
return orderShardMapper.selectByCursor(shard, lastId, lastTime, size);
}
复盘结论:分库分表之前,先把业务里所有"跨分片查询"列个清单,评估清楚代价,想好替代方案。 尤其是深分页、聚合统计、联表查询这三类,基本都要改方案。
第三件没想清楚的事:数据迁移和双写,比想象中难十倍
这是最痛的一件。我们当时的迁移方案是:停机窗口,凌晨两点到四点,业务暂停,跑迁移脚本。
想法很美好:两个小时代码里把数据从老表搬到新表,改完路由切过去。实际发生的事:
第一,迁移脚本跑不完。 3000万行数据,脚本跑了三个多小时,远超预估的两小时。窗口超了,业务被迫延长停机,白天上线的运营同事在群里骂。
第二,存量数据和新数据对不上。 停机期间有新订单进来(业务没完全停干净),老库里多了一批数据,迁移脚本没处理,导致新库缺数据,又花了一天手工补。
第三,验证不充分。 迁移完只抽查了少量数据,上线后陆续发现个别订单状态不一致,靠对账任务慢慢修复,前后折腾了一周。
如果再来一次,我会选择平滑迁移方案,不搞停机窗口:
- 双写阶段:新老库同时写入,通过消息队列异步同步
- 数据校验:写个对账任务,定期对比新老库数据,不一致的补偿修复
- 切读阶段:读流量逐步切到新库,先10%,再50%,观察无误后全量切换
- 下线老库:老库只读保留一段时间,确认无问题后清理
这套方案慢,但稳。分库分表这种操作,稳比快重要一百倍。
现在回头看,什么情况下其实不该分库分表
一年之后我有了个更清醒的认识:分库分表是最后手段,不是第一选择。
如果你的问题是"数据量大、查询慢",优先排查的应该是:
- 索引优化------是不是缺索引、索引没生效?很多"数据量大"其实是索引问题
- 归档历史数据------订单数据里,90天前的旧订单占了一大半,但查询频率极低,归档走冷存储,主表数据量直接降一半
- 读写分离------读多写少的场景,加从库扛读流量,比拆库简单得多
- 缓存------热点数据(如热销商品、热门文章)走缓存,数据库压力骤降
我们当时如果先做数据归档,订单主表从几千万行砍到一千万行以内,可能根本不用分库分表,或者能晚一年再拆,留出更充裕的时间设计。
复盘结论:分库分表之前,把其他优化手段先做一遍。 分库分表是复杂度最高、代价最大的方案,能不做就不做。
结尾
一年复盘下来,我对分库分表的态度从"技术方案"变成了"最后手段"。它解决的问题是真的,但它引入的复杂度------跨分片查询、数据迁移、全局唯一ID、分布式事务------也是实打实的。
如果你正在纠结要不要分库分表,我的建议是:
- 先做归档、索引、缓存、读写分离,能拖就拖
- 实在要拆,分片键跟着最高频查询走
- 迁移别搞停机窗口,用双写+对账的平滑方案
- 拆完把跨分片查询的替代方案提前想好
分库分表不是终点,是另一个复杂度的起点。想清楚再动手。
如果这篇对你有用,点个关注。下一篇写缓存穿透、击穿、雪崩------上个月我们的缓存系统就栽了一次,代价是数据库被打爆。