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 台应用。直到第二次扩容时才引入配置中心做路由热更新,但此时路由逻辑已经稳定,热更新带来的收益有限。早引入是负债,晚引入是按需还债,顺序不能反。

延伸阅读