数据库中间件与高可用
Java 架构师 · 高并发与架构
当单库性能(连接数、CPU)与容量(单表亿级)触顶,数据库中间件通过分库分表、读写分离把压力分散;高可用则靠主从复制、故障切换保障"数据不丢、服务不断"。这是高并发架构的底层支柱——但分库分表是"不得已而为之"的重型操作,拆分后运维成本陡增。本章深入分片路由原理、分片键选型、热点扩容、双写迁移与一致性校验,以及主从延迟的工程权衡。
一、为什么要中间件:先穷尽优化再拆分
动手分库分表前,必须确认已穷尽更轻的手段:
- SQL 优化 + 索引:最便宜的收益(避免全表扫描、深分页、N+1)。
- 读写分离:一主多从,分摊读压力。
- 升级硬件 / 换存储引擎:SSD、更大内存。
- 冷热分离:历史数据归档。
这些都到极限了,才上分库分表。架构师要在"性能收益"与"运维复杂度"间精算——拆分后,跨片 JOIN、分布式事务、扩容迁移都是长期成本。
二、读写分离与主从复制
一主多从,写走主库、读走从库。复制基于 binlog:
- 异步复制:主库提交即返回,从库后台追 binlog。性能最好,但有延迟(RPO>0,故障切换可能丢未同步数据)。
- 半同步复制:主库等至少一个从库 ACK 再返回。降低延迟但牺牲写性能(见 high-concurrency 章节主从延迟治理)。
故障切换:MHA / Orchestrator / 云 RDS 自动选新主。切换要点:原主降级为从并重连,避免"双主写"导致数据冲突。
三、分库分表:水平 vs 垂直
- 垂直拆分:按业务/字段拆(用户库、订单库;大字段拆出)。隔离热点,但不解决单表行数瓶颈。
- 水平拆分(分片):按分片键把一张大表拆到多个库/表。这是破解单表亿级瓶颈的根本手段。
ShardingSphere 分片配置示例:
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds${0..1}.t_order_${0..15} # 2库 × 16表
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db-inline # user_id % 2
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: tbl-inline # order_id % 16分片路由原理:SQL 进来 → 解析 → 提取分片键 → 计算目标库/表 → 改写 SQL → 路由执行 → 结果归并(Merge)。若 SQL 没带分片键,只能广播到所有分片再归并(全路由,性能极差)。
四、分片键选型:决定成败
| 分片键 | 优点 | 风险 |
|---|---|---|
| 用户 ID | 用户维度查询直路由(单分片) | 跨用户统计需广播 |
| 订单 ID | 订单维度均匀分散 | 按用户查需跨片(需冗余用户维度索引表) |
| 时间 | 冷热分离、易归档 | 写入倾斜到最新片(热点) |
选型原则:按最高频查询维度选分片键,并预判数据倾斜。基因法:把 user_id 的低位 bits 编入 order_id(如 order_id = 时间戳 + user_id 后 N 位),这样"按 order_id 查"也能反推 user_id 路由到正确分片,避免跨片。
五、热点扩容:从取模到一致性哈希
简单取模(hash(key) % N)的问题是:扩容(N 变 N+1)时几乎所有数据要重新分布 → 大规模迁移。解法:
- 一致性哈希(Consistent Hashing):环上虚拟节点,扩容只迁移相邻数据,影响局部。适合 KV 类。
- 范围分片:按时间/ID 范围,新范围直接新库,无需迁移旧数据(但易写入倾斜)。
- 双写迁移(见下):无论哪种路由,扩容都靠"双写过渡"平滑进行。
六、数据迁移与双写:可回滚的平滑上线
从单库迁到分库分表,绝不可"停服导数据"。标准双写迁移:
要点:
- 双写过渡期:新旧库同时写,读逐步切到新库。
- 全量(DataX/导数)+ 增量(Canal 追 binlog) 追平存量与增量。
- 一致性校验:定时比对新旧库(按主键抽样/全量),不一致告警修复。
- 可回滚:读切新库后若出问题,立刻切回旧库(旧库一直在双写,数据最新)。
分库分表的硬约束
- 跨片查询/JOIN 不可用:分片后跨库 JOIN 无法执行,需冗余字段或业务层聚合;避免用非分片键查询(全路由)。
- 分布式事务难:跨库事务无法单机 ACID,优先最终一致(见分布式事务章节)。
- 热点分片:分片键选错导致数据倾斜,单分片成瓶颈(如按时间分片,最新片被打爆)。
- 全局唯一 ID:自增失效,需分布式 ID(雪花/号段,见分布式理论章节)。
- 分页/排序/聚合:跨片
order by/count需各分片执行后归并,成本高。
七、高可用:主从 + 故障切换 + 多副本
- 主从复制(binlog):数据冗余,从库可顶上。
- 故障切换:原主宕机 → 选延迟最小从库升主(Orchestrator 基于拓扑感知选主,避免"无效主")。
- 多副本 + 仲裁:避免"两节点脑裂"——常用奇数节点(3/5)做选主仲裁(Raft 多数派思想)。
- 备份与 PITR:定期全量 + binlog,支持按时间点恢复(RPO 兜底)。
八、中间件选型
| 中间件 | 形态 | 特点 |
|---|---|---|
| ShardingSphere | JDBC 代理 / Proxy | 生态全、可透明接入、Proxy 模式无侵入 |
| MyCat | Proxy | 老牌、稳定、社区弱 |
| 云 DRDS / PolarDB | 云服务 | 托管、免运维、贵 |
经验:Java 应用优先 ShardingSphere-JDBC(无独立节点、性能好);需跨语言/无侵入用 Proxy 模式。
九、最佳实践
- 分片键按最高频查询维度选,用基因法兼顾多维度;预判倾斜。
- 用中间件做逻辑分片,业务代码尽量无感(ShardingSphere 拦截 SQL 改写)。
- 迁移走"双写 + 全量增量同步 + 校验 + 灰度切读 + 可回滚"。
- 强一致读(金融)显式路由主库;监控主从延迟,超阈值告警/切读。
本节小结
数据库中间件是突破单库瓶颈的利器:分库分表解决容量、读写分离解决读压力、主从复制+故障切换解决高可用。但拆分是重操作——应先穷尽索引/SQL/读写分离/硬件优化再拆。分片键选择决定成败(按高频查询维度 + 基因法防跨片),扩容用一致性哈希/双写平滑。迁移务必"双写+全量增量同步+一致性校验+灰度切读+可回滚",绝不停服导数据。分片后跨片 JOIN、分布式事务、热点倾斜成为长期约束。架构师要在"性能收益"与"运维复杂度"间精算,把分库分表当作最后一招而非第一招。
📚 官方文档与参考资料
- ShardingSphere 官方文档(分片/读写分离):https://shardingsphere.apache.org/document/current/cn/overview/
- Spring Cloud Alibaba:https://sca.aliyun.com/
- Canal(binlog 订阅,用于增量同步):https://github.com/alibaba/canal
- Kubernetes 文档(数据库容器化高可用参考):https://kubernetes.io/docs/
✏️ 练习与自测
- 设计题:订单表 20 亿行,设计分库分表方案(分片键、库表数量、路由算法),说明如何用"基因法"让"按用户查"和"按订单查"都能单分片路由,并避免热点。
- 设计题:给出"从单库平滑迁移到分库分表"的双写上线步骤(含全量/增量/校验/灰度/回滚),并写出每步的验收条件与回滚触发条件。
- 设计题:分片后"按用户统计近 30 天订单"这类跨片聚合查询如何做?列出可行方案(冗余汇总表/ES/流式计算)并对比优劣。
- 辨析题:为什么扩容时"取模分片"要大规模迁移数据?一致性哈希如何减少迁移范围?它又引入了什么新问题(虚拟节点/数据分布)?