分库分表一年后,我复盘了当时最该想清楚的三件事

我们订单库是去年做的分库分表,当时被数据量逼的------订单表两个月涨了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万行数据,脚本跑了三个多小时,远超预估的两小时。窗口超了,业务被迫延长停机,白天上线的运营同事在群里骂。

第二,存量数据和新数据对不上。 停机期间有新订单进来(业务没完全停干净),老库里多了一批数据,迁移脚本没处理,导致新库缺数据,又花了一天手工补。

第三,验证不充分。 迁移完只抽查了少量数据,上线后陆续发现个别订单状态不一致,靠对账任务慢慢修复,前后折腾了一周。

如果再来一次,我会选择平滑迁移方案,不搞停机窗口

  1. 双写阶段:新老库同时写入,通过消息队列异步同步
  2. 数据校验:写个对账任务,定期对比新老库数据,不一致的补偿修复
  3. 切读阶段:读流量逐步切到新库,先10%,再50%,观察无误后全量切换
  4. 下线老库:老库只读保留一段时间,确认无问题后清理

这套方案慢,但稳。分库分表这种操作,稳比快重要一百倍。


现在回头看,什么情况下其实不该分库分表

一年之后我有了个更清醒的认识:分库分表是最后手段,不是第一选择。

如果你的问题是"数据量大、查询慢",优先排查的应该是:

  1. 索引优化------是不是缺索引、索引没生效?很多"数据量大"其实是索引问题
  2. 归档历史数据------订单数据里,90天前的旧订单占了一大半,但查询频率极低,归档走冷存储,主表数据量直接降一半
  3. 读写分离------读多写少的场景,加从库扛读流量,比拆库简单得多
  4. 缓存------热点数据(如热销商品、热门文章)走缓存,数据库压力骤降

我们当时如果先做数据归档,订单主表从几千万行砍到一千万行以内,可能根本不用分库分表,或者能晚一年再拆,留出更充裕的时间设计。

复盘结论:分库分表之前,把其他优化手段先做一遍。 分库分表是复杂度最高、代价最大的方案,能不做就不做。


结尾

一年复盘下来,我对分库分表的态度从"技术方案"变成了"最后手段"。它解决的问题是真的,但它引入的复杂度------跨分片查询、数据迁移、全局唯一ID、分布式事务------也是实打实的。

如果你正在纠结要不要分库分表,我的建议是:

  1. 先做归档、索引、缓存、读写分离,能拖就拖
  2. 实在要拆,分片键跟着最高频查询走
  3. 迁移别搞停机窗口,用双写+对账的平滑方案
  4. 拆完把跨分片查询的替代方案提前想好

分库分表不是终点,是另一个复杂度的起点。想清楚再动手。

如果这篇对你有用,点个关注。下一篇写缓存穿透、击穿、雪崩------上个月我们的缓存系统就栽了一次,代价是数据库被打爆。

相关推荐
岁月如歌77861 小时前
顺序消息与幂等消费完全指南:从队列有序到接口幂等
java·后端·架构
秋名RG1 小时前
Java 泛型深度解析:从类型安全到现代实践(JDK 21 视角)
java·windows·安全
合橱瑰1 小时前
从“假智能”到“真闭环”:Go 向量推理引擎的零硬编码调优实践
后端·go
SamDeepThinking1 小时前
不改逻辑、不拆方法:仅靠「调整代码顺序」提升可读性
java·后端·程序员
泡海椒1 小时前
适配老旧项目:JQuick-Java兼容Java8+环境改造迁移实战指南
后端
IT_陈寒1 小时前
Java的HashMap竟然不是线程安全的,现在才知道!
前端·人工智能·后端
IT_陈寒1 小时前
React hooks闭包陷阱让我加了一宿班
前端·人工智能·后端
不一样的少年_1 小时前
设计稿里的图片明明很清晰,为什么到了手机上却糊了?一文讲透 DPR、压缩与格式选择
前端·后端·图片资源
卷无止境1 小时前
当"精简主义"住进AI编程助手:Ponytail深度解读
后端·python·fastapi