1.4 技术栈对齐:中间件与云产品
分库分表从来不是 MySQL 一个人的事。路由算谁做、跨库事务谁保证、冷数据往哪放、慢查询去哪看,这四个问题各需要一件工具。这一节把我们最终对齐的技术栈摆出来,并解释每一件工具为什么是它而不是别的。
场景:四个问题各需要一件工具
16 个分片部署完成只是搭好了舞台,真正的戏在应用层。第一个问题是路由:应用拿到订单号怎么知道查哪个库。第二个问题是事务:下单要同时写订单表与库存表,库存与订单不在一个分片时怎么办。第三个问题是归档:三年前的订单放哪。第四个问题是观测:16 个分片的慢查询怎么集中看。
| 问题 | 备选方案 | 最终选择 | 选择理由 |
|---|---|---|---|
| 路由层 | ShardingSphere-JDBC / Proxy / 自研 | JDBC 模式 | 零额外集群,随应用部署 |
| 跨库事务 | XA / TCC / 消息最终一致 | 消息加对账 | 与订单业务容错模型匹配 |
| 冷数据归档 | 冷备实例 / COS / 归档表 | COS 加冷表 | 成本最低且可按行取回 |
| 慢查询观测 | 自建收集 / CLS | CLS 日志服务 | 分片日志统一检索免运维 |
| 路由缓存 | 本地缓存 / Redis | Redis | 多实例共享路由信息 |
中间件曾经在 ShardingSphere 的 JDBC 与 Proxy 两种模式之间犹豫很久。Proxy 模式对应用零侵入,但多一跳网络且自身需要集群高可用;JDBC 模式每个应用实例各自路由,省掉了 Proxy 层。订单系统应用节点只有 12 台,JDBC 模式的运维成本更低,这一条决定了方案。
两种模式的差异值得多说一句。Proxy 模式下所有 SQL 先经过代理集群,代理集群自身就是新的单点风险,需要 3 节点选举;JDBC 模式把路由逻辑下沉到每个应用进程,数据库连接是「应用直连分片」,少了中间一跳的网络延迟,我们的下单耗时因此降了 4 毫秒。代价是所有应用都要引入 ShardingSphere 依赖,不同语言(Java 之外的 Go 服务)要自行解析路由规则——订单系统里有一个 Go 写的物流回调服务就遇到了这个问题,最后给它单独维护了一份轻量路由逻辑,或者让它走 Java 服务转发。
方案:各组件在整个链路中的位置
整个技术栈的分工是:应用通过 ShardingSphere-JDBC 路由到 16 个分片;分片键到物理库的二级路由(比如店铺到分片的映射)放 Redis;分布式事务用本地消息表加定时对账;冷数据按月归档到 COS;16 个分片的慢查询日志接到 CLS 统一检索。
Redis 在这里承担的不是缓存热点数据,而是「路由信息缓存」——有些业务查询不带分片键(比如按店铺查订单),需要先查一次店铺到分片的映射表,这步走 Redis 比每次查 MySQL 字典表快一个量级,详见 2.4 节。Redis 的选型与容量规划可以参考 Go 实战:IM 系统 中关于缓存层治理的内容,那本书对 Redis 集群的坑讲得很细。
CLS 的作用被低估了。分库之后最怕的是「某个分片慢了但你不知道是哪个」,CLS 把 16 个分片的慢日志统一到一个主题,配合 4.3 节的治理流程,定位慢查询从半小时缩到两分钟。
CLS 接入的关键是日志投递的「来源标记」。每个分片投递慢日志时必须带上分片编号,否则 16 路日志混在一起还是定位不了。我们用 Loglistener 的 metadata 字段自动注入机器名与分片编号,检索时 key:ds_07 就能直接过滤出第 7 号分片的慢 SQL。检索语法用 CLS 的检索分析语句,比在 16 台机器上分别 grep 强得多。
整个技术栈的演进也是渐进的:第一版只有 ShardingSphere,跨库一致性靠手工对账;第二版加上本地消息表;第三版引入 Redis 路由缓存;第四版补齐 CLS 与 COS 归档。每一版都是被具体问题推着走的,没有一次是为了「架构先进」而引入组件。如果是新项目从零开始,可以参考 k8s 实战:发布流水线 的思路,把中间件版本管理与灰度发布一起规划。
配置:JDBC 模式的接入要点
ShardingSphere-JDBC 接入最关键的一条是数据源池的配置:每个分片对应一个独立连接池,连接池总数等于分片数乘以应用节点数,很容易把分片的连接数打满。下面是我们在订单写入服务上的配置片段:
dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
jdbcUrl: 'jdbc:mysql://ds0.internal:3306/order'
username: app_order
password: '***'
connectionTimeout: 3000
maximumPoolSize: 20
minimumIdle: 5
16 个分片、12 台应用节点、每池 20 连接,单分片最大连接 240,远低于实例 3000 的上限,但高峰期已经远超必要值。后来把 maximumPoolSize 降到 8 并配合连接监控,分片压力反而更平稳——连接数与吞吐不是线性关系,连接过多会增加锁竞争。
连接池的容量估算公式可以记一下:单实例需要的连接数约等于「峰值 QPS 乘以平均 SQL 耗时(秒)再加 20% 余量」。我们峰值 400 QPS、平均 SQL 耗时 8 毫秒,算出来约 4 条活跃连接就够了,余量后配 8 条。剩下 200 条连接其实都在空闲排队,纯属浪费。
| 组件 | 角色 | 部署形态 | 常见坑 |
|---|---|---|---|
| ShardingSphere-JDBC | 分片路由 | 随应用 12 节点 | 版本兼容性、事务语义 |
| Redis 路由缓存 | 二级路由 | 3 主 3 从 | 迁移期缓存漂移 |
| 消息队列 | 最终一致 | 3 节点 | 消息重复投递 |
| COS 冷归档 | 冷数据 | 托管 | 取回延迟分钟级 |
| CLS 日志 | 慢查询归集 | 托管 | 投递延迟与成本 |
| 配置项 | 初始值 | 调整后 | 依据 |
|---|---|---|---|
| maximumPoolSize | 20 | 8 | 峰值并发推算加余量 |
| minimumIdle | 5 | 2 | 减少常驻空闲连接 |
| connectionTimeout | 3000 | 1000 | 快速暴露连接耗尽故障 |
| idleTimeout | 默认 | 60s | 及时回收空闲连接 |
踩坑:中间件版本与 MySQL 大版本不匹配
上线预发时遇到一个诡异的故障:部分写入语句报语法错误,但同样的 SQL 直连 MySQL 正常。排查下来是 ShardingSphere 早期版本对 MySQL 8.0 的 CHECK TABLE 类语句解析不完整,升级小版本后解决。教训是中间件与数据库版本要做兼容性矩阵验证,不能默认支持。
第二个坑是事务语义变化。ShardingSphere 默认不开启 XA 事务,跨库写入在异常时只保证「每个库各自提交成功」,不保证全局一致。我们有一段时间误以为开了事务就安全,直到对账发现一笔订单库存扣了但订单没落库,才补上本地消息表方案——这段经历在 3.2 节完整展开。
第三个坑是 Redis 路由缓存的「数据漂移」。店铺迁移分片(扩容时会发生)如果忘了刷新缓存,应用还会往旧分片写数据,造成数据裂脑。我们给路由缓存设置了双重失效:迁移任务执行前先主动删缓存 Key,Key 本身再加 5 分钟 TTL 兜底。任何缓存都不能只有「写时更新」没有「失效兜底」,这是分散式系统最常见的隐性不一致来源。
小结
技术栈对齐的核心是「每件事交给最合适的组件」:路由交给 ShardingSphere,二级路由交给 Redis,最终一致性交给消息加对账,归档交给 COS,观测交给 CLS。引入的组件越多,版本兼容性与事务语义的坑就越多,上线前的兼容性矩阵验证不能省。
技术栈的最后一个原则是「能晚引入就晚引入」。我们在分库第一年没有上配置中心,16 个分片的连接信息写在本地配置里,改一次要发版 12 台应用。直到第二次扩容时才引入配置中心做路由热更新,但此时路由逻辑已经稳定,热更新带来的收益有限。早引入是负债,晚引入是按需还债,顺序不能反。
延伸阅读
- Go 实战:IM 系统:Redis 集群与连接池治理的实战经验
- Go 实战:微服务 CPS:微服务拆分后中间件选型的对照参考
- MySQL 实战:分库分表订单系统:回到本书,第二篇正式进入分片键设计
