2026/11/3 16:50:36

Sharding-JDBC分库分表实战:从双十一崩溃到支撑4500万订单

Sharding-JDBC分库分表实战:从双十一崩溃到支撑4500万订单 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符号位固定为041bit时间戳支持69年5bit数据中心ID5bit机器ID12bit序列号每节点每毫秒4096个ID4. 查询优化实践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%使用PrometheusGranfa搭建的监控看板# 分片查询分布 sum(rate(shardingsphere_statement_total{typeSELECT}[1m])) by (datasource) # 慢查询统计 sum(shardingsphere_statement_latency_milliseconds_bucket{le500}) by (sql_type)6. 经验总结与避坑指南6.1 关键注意事项分片键选择必须谨慎避免使用会更新的字段优先考虑最常用查询条件确保数据分布均匀性分布式事务的代价柔性事务的成功率通常为99.5%-99.9%需要设计完善的补偿机制数据迁移的黄金法则保留完整回滚方案数据校验不能只抽样灰度发布必须可控6.2 性能对比数据实施前后的关键指标对比指标分片前分片后写入TPS1,2008,500平均查询延迟1,200ms85ms最大连接数800200存储空间350GB25GB/分片备份时间6小时45分钟7. 扩展优化方向当前架构仍可优化的点冷热数据分离将3个月前的订单归档到OSS读写分离每个分片配置1主2从弹性扩缩容基于Kubernetes实现自动分片平衡多租户支持在分片键中加入租户ID维度一个正在测试的自动扩容方案// 动态分片算法示例 public class DynamicShardingAlgorithm implements PreciseShardingAlgorithmString { Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueString 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以内。最大的收获是认识到分片不是银弹必须配合合理的查询设计、监控体系和容量规划才能发挥最大价值。