☰
Sharding-JDBC分库分表实战:从双十一崩溃到支撑4500万订单
2026/11/3 16:50:31 网站建设 项目流程

1. 项目背景与挑战

去年双十一大促期间,我们团队负责的淘客系统遭遇了严重的数据库性能瓶颈。当天订单量突破3000万单,核心订单表的单日数据写入量达到15GB,查询响应时间从平时的200ms飙升到5秒以上。这直接导致结算延迟和用户投诉,让我们不得不开始认真考虑分库分表方案。

订单表的数据增长曲线非常典型:上线3年累计数据量已达8.7亿条,单表体积超过350GB。即使我们使用了SSD存储和MySQL读写分离,简单查询的响应时间仍然超过1秒,更不用说复杂的多表关联查询了。经过压力测试,我们发现单表数据量超过5000万条时,即使有索引,查询性能也会出现断崖式下降。

2. 技术选型与方案设计

2.1 主流分库分表方案对比

我们重点评估了三种主流方案:

  1. 应用层分片:如Sharding-JDBC、MyCat
  2. 中间件代理:如MySQL Router、ProxySQL
  3. 数据库原生方案:如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 数据迁移方案

采用"双写+增量同步"的平滑迁移方案:

  1. 新老系统并行运行2周
  2. 使用Canal监听binlog同步增量数据
  3. 开发数据校验工具对比新旧数据一致性
  4. 灰度切流,按用户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 分布式事务处理

结合本地消息表实现最终一致性:

  1. 订单创建时写入本地事务表
  2. 通过定时任务补偿失败的消息
  3. 设置最大重试次数和告警机制

关键代码实现:

@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工具发现的典型问题:

  1. 跨分片COUNT(*)查询
    • 解决方案:维护计数表,通过触发器更新
  2. 非分片键条件查询
    • 解决方案:建立广播表或冗余字段
  3. 大事务导致锁等待
    • 解决方案:拆分事务,引入柔性事务

5.2 监控体系建设

关键监控指标:

  1. 分片键分布均匀性(标准差<15%)
  2. 单分片数据量(预警阈值5000万)
  3. 跨分片查询比例(控制在5%以内)
  4. 分布式事务成功率(>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 关键注意事项

  1. 分片键选择必须谨慎:

    • 避免使用会更新的字段
    • 优先考虑最常用查询条件
    • 确保数据分布均匀性
  2. 分布式事务的代价:

    • 柔性事务的成功率通常为99.5%-99.9%
    • 需要设计完善的补偿机制
  3. 数据迁移的黄金法则:

    • 保留完整回滚方案
    • 数据校验不能只抽样
    • 灰度发布必须可控

6.2 性能对比数据

实施前后的关键指标对比:

指标分片前分片后
写入TPS1,2008,500
平均查询延迟1,200ms85ms
最大连接数800200
存储空间350GB25GB/分片
备份时间6小时45分钟

7. 扩展优化方向

当前架构仍可优化的点:

  1. 冷热数据分离:将3个月前的订单归档到OSS
  2. 读写分离:每个分片配置1主2从
  3. 弹性扩缩容:基于Kubernetes实现自动分片平衡
  4. 多租户支持:在分片键中加入租户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以内。最大的收获是认识到:分片不是银弹,必须配合合理的查询设计、监控体系和容量规划才能发挥最大价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询