2023 年那会儿,公司业务量涨得挺猛,有几张核心表的数据量往亿级走。业务方开始焦虑:要不要分库分表?要不要上 ShardingSphere?

DAL 小组花了大概半年做调研和小范围试点,最后给出的结论是:现阶段不上分库分表中间件,自研读写分离 + Redis 缓存够用。

这篇就把调研过程和决策逻辑摊开讲。不打算给”该不该分库分表”下通用结论,只是讲我们当时怎么权衡的。

先搞清楚问题

调研的第一步不是看方案,是搞清楚到底有没有问题。

业务方说”数据量大了”,但”大了”不等于”有问题”。MySQL 单表能扛多少数据?这个问题的答案取决于很多因素:

  • 表结构设计合不合理
  • 索引建得好不好
  • 查询模式是点查还是范围查、是 OLTP 还是 OLAP
  • 硬件配置

我们拉了一下当时的数据:

1
2
3
4
5
-- 看几张核心表的数据量
SELECT table_name, table_rows, ROUND(data_length/1024/1024, 2) AS data_mb,
ROUND(index_length/1024/1024, 2) AS index_mb
FROM information_schema.tables
WHERE table_schema = 't_xxx';

结果:最大的表 1.2 亿行,数据 45GB,索引 18GB。B+树高度 3-4 层,单次点查走索引的话延迟在 10ms 以内。

说实话,这个量级 MySQL 单表完全扛得住。1 亿行在 InnoDB 里不算什么,只要索引和查询模式没大问题。

很多时候”数据量大了”其实是”慢查询多了”,根儿在索引和 SQL 写得烂,不在数据量本身。

分库分表的代价

但既然要调研,就得把”如果分了会怎样”想清楚。分库分表的代价比大多数人想象的大得多。

1. 事务没了

跨库的事务 MySQL 原生不支持。要么用 XA 分布式事务(性能差、复杂),要么走 TCC / Saga 的应用层补偿(开发量大)。

我们核心交易链路强依赖事务。如果分库了,一笔订单涉及订单库、库存库、账户库,原来一个 BEGIN ... COMMIT 搞定的事,现在要写一堆补偿逻辑。开发和维护成本翻几倍。

2. JOIN 没了

跨库 JOIN 基本做不了。原来一条 SQL 能查出来的数据,分库后要么走多次查询再在应用层组装,要么做数据冗余。

数据冗余意味着数据一致性维护成本,A 库的数据更新了,B 库的冗余副本怎么同步?又是 binlog 订阅、异步同步、最终一致性那一套。

3. 运维复杂度飙升

  • 备份恢复:从单库备份变成多库协调备份。
  • DDL 变更:一个 ALTER TABLE 要在所有分片上执行。
  • 数据迁移:扩容、缩容、rebalance,每一步都是大工程。
  • 监控:要监控每个分片的健康状态。

这些事情不是 DBA 加几个人就能解决的,整个运维体系要重构。

4. ShardingSphere 的坑

我们做了 ShardingSphere-JDBC 的小范围试点。功能确实强,但踩了几个坑:

  • 复杂 SQL 解析偶有问题。子查询嵌套三层以上,路由结果偶尔不对。
  • 运维工具链不成熟。ShardingSphere-Scaling 做数据迁移,当时还不太稳定。
  • 学习成本高。团队里能玩转的人不多,出了问题排查困难。

不分库,靠什么扛

既然决定不分,那怎么扛住业务增长?我们的方案是三板斧:优化现有查询、读写分离、Redis 缓存。

第一斧:SQL 和索引优化

这个前面慢查询治理那篇讲过了。EXPLAIN + 加索引,把大量全表扫描的慢查询优化掉。光这一步,数据库压力就降了一半。

很多业务方以为”数据库慢了就该分库”,其实 80% 的慢是 SQL 写得烂。把 SQL 治好了,单表扛到 2-3 亿行没压力。

第二斧:读写分离

主写从读,前面也讲过了。读流量大头在报表和列表查询,这些走从库,主库压力直接减半。

第三斧:Redis 缓存

热数据放 Redis,MySQL 只在缓存 miss 的时候被命中。

1
2
3
4
5
6
7
8
9
10
11
# 典型的 cache-aside 模式
def get_order(order_id):
# 先查 Redis
cached = redis.get(f"order:{order_id}")
if cached:
return json.loads(cached)
# 缓存 miss,查 MySQL
order = db.query(Order).filter_by(id=order_id).first()
if order:
redis.setex(f"order:{order_id}", 300, json.dumps(order.to_dict()))
return order

缓存策略上我们的经验:

  • 过期时间加随机抖动,防止缓存雪崩。
  • 热点 key 单独保护,用本地缓存兜一层。
  • 缓存与数据库一致性靠 binlog 订阅 + 主动删除,不做复杂更新。

缓存的本质是用内存换 CPU 和 IO。内存比硬盘贵,但比分库分表便宜得多。

什么时候才该分

我们没有说”永远不分库分表”。当时给了一个明确的触发条件:

  • 单表数据超过 5 亿行,且通过 SQL 优化、读写分离、缓存三板斧仍无法满足性能要求。
  • 出现明确的垂直拆分边界,某些业务模块的数据访问模式和其他模块差异巨大,可以独立成一个库。

这两个条件目前都没触发。等到触发的那一天,我们会重新评估。

决策的逻辑

把决策逻辑写清楚:

方案 成本 收益 风险
分库分表 + 中间件 高(开发+运维翻倍) 理论上的线性扩展 事务/JOIN 丢失、运维复杂
SQL优化 + 读写分离 + 缓存 低(现有体系内) 够用,扛到 2-3 亿行 缓存一致性、从库延迟

够用就别加复杂度,不是说先进方案就该用。

软件工程里有一条不成文的规律:复杂度是债务,能不借就不借。分库分表借的是一大笔复杂度债,不到万不得已不要借。

结论

调研报告交上去之后,CTO 拍板:现阶段不分。DAL 小组继续把读写分离和缓存做扎实,把慢查询治理持续推下去。

回头看,这个决策是对的。又过了一年多,业务还在涨,但靠优化和缓存扛住了,没有分库分表带来的运维噩梦。

很多技术决策的关键在于”当前阶段的成本和收益哪个最匹配”,而不是”哪个方案更先进”。这是我做 DAL 调研最大的体会。