1. 分库分表路由组件的基本概念
分库分表路由组件是分布式数据库架构中的核心基础设施,它负责将数据请求正确地路由到对应的物理数据库或数据表。随着业务规模扩大,单机数据库在存储容量和性能上都会遇到瓶颈,这时就需要通过分库分表来水平扩展数据库能力。
路由组件的主要职责包括:
- 解析SQL语句中的分片键(Sharding Key)
- 根据分片算法计算目标库表位置
- 重写SQL语句中的表名
- 合并跨分片的查询结果
2. 路由组件的核心设计要素
2.1 分片策略选择
常见的分片策略包括:
- 哈希分片:对分片键取模,数据均匀分布但难以范围查询
- 范围分片:按ID范围划分,支持范围查询但可能数据倾斜
- 时间分片:按时间维度划分,适合时序数据
- 目录分片:维护分片映射表,灵活但需要额外存储
在电商订单系统中,我们采用了"用户ID哈希+订单创建时间范围"的复合分片策略。用户查询时先按用户ID哈希定位库,再按时间范围定位表,既保证了用户维度的查询效率,又实现了数据的冷热分离。
2.2 分布式ID生成方案
分库分表环境下需要全局唯一的ID生成器,常用方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| UUID | 实现简单 | 无序存储,索引效率低 | 小规模系统 |
| 数据库序列 | 绝对有序 | 有单点瓶颈 | 中小规模系统 |
| Snowflake | 高性能分布式 | 依赖系统时钟 | 大规模分布式系统 |
| Leaf | 高可用 | 架构复杂 | 超大规模系统 |
我们最终选择了改良版Snowflake算法,在64位中分配:
- 1位符号位(固定0)
- 41位时间戳(支持69年)
- 10位工作机器ID(支持1024节点)
- 12位序列号(每毫秒4096个ID)
3. 路由组件的具体实现
3.1 SQL解析与改写
路由组件需要解析原始SQL并改写表名。以MyBatis项目为例,我们通过实现SqlSource接口来自定义SQL解析逻辑:
java复制public class ShardingSqlSource implements SqlSource {
private final SqlSource original;
private final ShardingStrategy strategy;
@Override
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = original.getBoundSql(parameterObject);
String newSql = rewriteSql(boundSql.getSql(), parameterObject);
return new BoundSql(...);
}
private String rewriteSql(String originalSql, Object params) {
// 解析分片键值
Object shardValue = extractShardValue(originalSql, params);
// 计算物理表名
String physicalTable = strategy.calculateTable(shardValue);
// 替换逻辑表名为物理表名
return originalSql.replace("order", physicalTable);
}
}
3.2 分片路由执行流程
完整的请求处理流程如下:
- 拦截DAO层方法调用
- 解析方法参数获取分片键值
- 通过分片算法计算目标数据源和表名
- 创建分片数据源连接
- 执行改写后的SQL
- 对跨分片查询进行结果归并
4. 生产环境中的实践经验
4.1 热点数据问题处理
在大促期间,某些热门商品的数据会集中到同一分片,我们通过以下方案缓解:
- 动态分片:检测到热点时自动分裂分片
- 本地缓存:在应用层缓存热点数据
- 读写分离:将读请求路由到从库
4.2 分布式事务方案
对于跨分片的写操作,我们采用柔性事务保证最终一致性:
- 记录事务日志
- 异步执行补偿操作
- 定时对账修复
具体实现基于RocketMQ的事务消息:
java复制// 发送预备消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(prepareMsg, arg);
// 本地事务执行
@Transactional
public boolean executeLocalTransaction(Message msg, Object arg) {
try {
// 1. 写主库
orderDAO.insert(arg);
// 2. 写事务日志
txLogDAO.insert(buildLog(msg));
return true;
} catch(Exception e) {
return false;
}
}
// 事务回查
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
TxLog log = txLogDAO.selectByMsgId(msg.getMsgId());
return log.getStatus() == CONFIRMED ? COMMIT_MESSAGE : ROLLBACK_MESSAGE;
}
5. 性能优化关键点
5.1 连接池配置建议
每个分片数据源需要独立配置连接池,我们的最佳实践:
yaml复制spring:
datasource:
shard1:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
shard2:
hikari:
maximum-pool-size: 20
# ...相同配置
5.2 慢查询监控方案
我们在路由层拦截所有SQL执行,通过ELK收集分析慢查询:
- 拦截SQL执行请求
- 记录执行时间超过阈值的查询
- 上报到Logstash
- 在Kibana展示慢查询报表
关键实现代码:
java复制@Around("execution(* javax.sql.DataSource.getConnection(..))")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > SLOW_THRESHOLD) {
logSlowQuery(pjp, cost);
}
}
}
6. 上线后的运维要点
6.1 数据迁移方案
从单库迁移到分库分表的步骤:
- 双写旧库和新库
- 全量数据迁移
- 增量数据同步
- 数据一致性校验
- 流量切换
我们开发了专用的数据迁移工具,支持:
- 多线程并行迁移
- 断点续传
- 自动重试机制
- 行级校验比对
6.2 监控指标设计
关键监控指标包括:
- 分片查询命中率
- 跨分片查询比例
- 单分片负载均衡情况
- 路由组件自身性能指标
使用Prometheus采集的指标示例:
code复制# HELP sharding_query_total Total queries by shard
# TYPE sharding_query_total counter
sharding_query_total{shard="shard1"} 12345
sharding_query_total{shard="shard2"} 11876
# HELP sharding_latency_seconds Query latency by shard
# TYPE sharding_latency_seconds histogram
sharding_latency_seconds_bucket{shard="shard1",le="0.1"} 11234
sharding_latency_seconds_bucket{shard="shard1",le="0.5"} 12345
分库分表路由组件的引入需要根据业务特点进行深度定制,在数据分布均衡性、查询性能和运维复杂度之间找到最佳平衡点。经过多个项目的实践验证,合理的分片策略设计配合完善的监控体系,可以使系统支持百万级QPS和PB级数据存储。
