1. 项目背景与挑战
去年双十一大促期间,我们团队负责的淘客系统遭遇了严重的数据库性能瓶颈。当天订单量突破3000万单,核心订单表的单日数据写入量达到15GB,查询响应时间从平时的200ms飙升到5秒以上。这直接导致结算延迟和用户投诉,让我们不得不开始认真考虑分库分表方案。
订单表的数据增长曲线非常典型:上线3年累计数据量已达8.7亿条,单表体积超过350GB。即使我们使用了SSD存储和MySQL读写分离,简单查询的响应时间仍然超过1秒,更不用说复杂的多表关联查询了。经过压力测试,我们发现单表数据量超过5000万条时,即使有索引,查询性能也会出现断崖式下降。
2. 技术选型与方案设计
2.1 主流分库分表方案对比
我们重点评估了三种主流方案:
- 应用层分片:如Sharding-JDBC、MyCat
- 中间件代理:如MySQL Router、ProxySQL
- 数据库原生方案:如MySQL Cluster、Vitess
最终选择Sharding-JDBC基于以下考虑:
- 无proxy层,性能损耗最小(测试显示延迟增加<15%)
- 与Spring生态完美集成,学习成本低
- 支持柔性事务,适合我们的业务场景
- 社区活跃,阿里云提供商业支持
2.2 分片策略设计
核心订单表的分片键选择经过多次论证:
- 备选字段:user_id(用户ID)、order_id(订单ID)、create_time(创建时间)
- 最终选择order_id作为分片键,原因:
- 查询90%通过order_id定位
- 避免用户订单全部分布在同一分片
- 使用Snowflake算法生成的ID自带时间序
分片算法采用"hash取模+范围查询"组合:
// 分库分表算法示例 String orderId = "123456789012345678"; int dbCount = 16; // 分库数 int tableCount = 64; // 分表数 long hash = Hashing.murmur3_128().hashString(orderId, UTF_8).asLong(); int dbIndex = Math.abs((int)(hash % dbCount)); int tableIndex = Math.abs((int)(hash % tableCount));3. 具体实施过程
3.1 数据迁移方案
采用"双写+增量同步"的平滑迁移方案:
- 新老系统并行运行2周
- 使用Canal监听binlog同步增量数据
- 开发数据校验工具对比新旧数据一致性
- 灰度切流,按用户ID逐步迁移
迁移过程中的关键配置:
# Sharding-JDBC配置示例 spring: shardingsphere: datasource: names: ds0,ds1,...,ds15 sharding: tables: t_order: actual-data-nodes: ds$->{0..15}.t_order_$->{0..63} database-strategy: inline: sharding-column: order_id algorithm-expression: ds$->{Math.abs(order_id.hashCode() % 16)} table-strategy: inline: sharding-column: order_id algorithm-expression: t_order_$->{Math.abs(order_id.hashCode() % 64)}3.2 分布式ID生成
采用改良版Snowflake算法:
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000调整了各部分的bit分配:
- 1bit符号位(固定为0)
- 41bit时间戳(支持69年)
- 5bit数据中心ID
- 5bit机器ID
- 12bit序列号(每节点每毫秒4096个ID)
4. 查询优化实践
4.1 分页查询处理
跨分片分页的解决方案:
// 使用内存归并方式处理分页 ShardingHintManager.getInstance().setShardingHintValue( new HintShardingValue<>("t_order", "order_id", Collections.singletonList("ds0.t_order_0"))); // 实际执行时会被改写为: // SELECT * FROM t_order_0 ORDER BY create_time DESC LIMIT 0,10 // UNION ALL // SELECT * FROM t_order_1 ORDER BY create_time DESC LIMIT 0,10 // ...4.2 分布式事务处理
结合本地消息表实现最终一致性:
- 订单创建时写入本地事务表
- 通过定时任务补偿失败的消息
- 设置最大重试次数和告警机制
关键代码实现:
@Transactional public void createOrder(Order order) { // 1. 写入订单 orderMapper.insert(order); // 2. 写入本地消息表 MessageRecord msg = new MessageRecord(); msg.setBizId(order.getOrderId()); msg.setContent(JSON.toJSONString(order)); messageMapper.insert(msg); // 3. 发送MQ消息 rocketMQTemplate.asyncSend("order_topic", MessageBuilder.withPayload(order).build(), new SendCallback() { @Override public void onSuccess(SendResult sendResult) { messageMapper.updateStatus(msg.getId(), "SENT"); } //...错误处理省略 }); }5. 性能优化与监控
5.1 慢查询治理
通过APM工具发现的典型问题:
- 跨分片COUNT(*)查询
- 解决方案:维护计数表,通过触发器更新
- 非分片键条件查询
- 解决方案:建立广播表或冗余字段
- 大事务导致锁等待
- 解决方案:拆分事务,引入柔性事务
5.2 监控体系建设
关键监控指标:
- 分片键分布均匀性(标准差<15%)
- 单分片数据量(预警阈值5000万)
- 跨分片查询比例(控制在5%以内)
- 分布式事务成功率(>99.9%)
使用Prometheus+Granfa搭建的监控看板:
# 分片查询分布 sum(rate(shardingsphere_statement_total{type="SELECT"}[1m])) by (datasource) # 慢查询统计 sum(shardingsphere_statement_latency_milliseconds_bucket{le="500"}) by (sql_type)6. 经验总结与避坑指南
6.1 关键注意事项
分片键选择必须谨慎:
- 避免使用会更新的字段
- 优先考虑最常用查询条件
- 确保数据分布均匀性
分布式事务的代价:
- 柔性事务的成功率通常为99.5%-99.9%
- 需要设计完善的补偿机制
数据迁移的黄金法则:
- 保留完整回滚方案
- 数据校验不能只抽样
- 灰度发布必须可控
6.2 性能对比数据
实施前后的关键指标对比:
| 指标 | 分片前 | 分片后 |
|---|---|---|
| 写入TPS | 1,200 | 8,500 |
| 平均查询延迟 | 1,200ms | 85ms |
| 最大连接数 | 800 | 200 |
| 存储空间 | 350GB | 25GB/分片 |
| 备份时间 | 6小时 | 45分钟 |
7. 扩展优化方向
当前架构仍可优化的点:
- 冷热数据分离:将3个月前的订单归档到OSS
- 读写分离:每个分片配置1主2从
- 弹性扩缩容:基于Kubernetes实现自动分片平衡
- 多租户支持:在分片键中加入租户ID维度
一个正在测试的自动扩容方案:
// 动态分片算法示例 public class DynamicShardingAlgorithm implements PreciseShardingAlgorithm<String> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) { // 从配置中心获取最新分片数 int currentShardCount = ConfigCenter.getShardCount(); // 计算目标分片 long hash = Hashing.murmur3_32().hashString( shardingValue.getValue(), UTF_8).asInt(); int shardIndex = Math.abs((int)(hash % currentShardCount)); return "ds_" + shardIndex; } }这套分库分表方案实施后,系统成功支撑了去年双十二期间单日4500万订单的业务峰值,平均响应时间保持在200ms以内。最大的收获是认识到:分片不是银弹,必须配合合理的查询设计、监控体系和容量规划才能发挥最大价值。