电商订单系统分库分表实战:Sharding-JDBC与Mybatis-Plus深度整合
去年双十一,我们团队负责的电商平台订单系统在流量洪峰下出现了严重的数据库性能瓶颈。单表数据量突破5000万条后,查询响应时间从毫秒级骤增至秒级,甚至出现多次数据库连接耗尽的情况。这次事故让我们彻底意识到:手动分库分表不仅效率低下,而且难以应对业务快速增长的需求。经过技术选型,我们最终采用Sharding-JDBC+Mybatis-Plus组合方案,实现了订单系统的平滑扩容。本文将分享这套方案的完整落地过程。
1. 为什么电商订单必须分库分表?
电商订单系统通常面临三个典型挑战:
- 数据量爆炸式增长:日均订单量超过10万时,单表存储很快就会达到性能临界点
- 高并发读写压力:大促期间QPS可能激增百倍,单数据库实例难以承受
- 复杂查询需求:需要支持按用户、时间、商品等多维度查询
我们曾尝试过以下传统优化手段:
// 典型的分表查询伪代码(不推荐) public Order getOrder(Long orderId) { int tableSuffix = orderId % 16; // 手动计算表后缀 String sql = "SELECT * FROM order_" + tableSuffix + " WHERE id=?"; // 执行查询... }这种方案存在明显缺陷:
- 业务代码与分片逻辑强耦合
- 跨分片查询实现复杂
- 扩容需要修改代码并迁移数据
Sharding-JDBC的核心价值在于它作为JDBC层的代理,对业务代码完全透明。开发者只需关注业务逻辑,分片规则通过配置声明,无需硬编码。
2. 分片策略设计与实战配置
2.1 订单系统的分片维度选择
电商订单最合理的分片键是用户ID(userId),因为:
- 80%的查询都是基于用户维度
- 能保证同一用户的订单落在相同分片
- 避免跨分片事务问题
我们采用二级分片策略:
- 按userId分库(2个库)
- 按orderId分表(每个库2张表)
分片算法配置示例:
spring: shardingsphere: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.order_$->{0..1} database-strategy: inline: sharding-column: user_id algorithm-expression: ds$->{user_id % 2} table-strategy: inline: sharding-column: order_id algorithm-expression: order_$->{order_id % 2}注意:实际生产环境建议使用更复杂的分片算法,如范围分片或复合分片
2.2 分布式主键的最佳实践
订单ID必须满足:
- 全局唯一
- 趋势递增(有利于索引优化)
- 无业务含义
我们选择Snowflake算法,配置方式:
spring: shardingsphere: sharding: tables: t_order: key-generator: column: order_id type: SNOWFLAKE props: worker.id: ${server.worker-id}3. Mybatis-Plus无缝整合技巧
3.1 实体类与Mapper标准写法
@Data @TableName("t_order") // 逻辑表名 public class Order { @TableId(type = IdType.ASSIGN_ID) // 分布式ID private Long orderId; private Long userId; private BigDecimal amount; // 其他字段... } @Mapper public interface OrderMapper extends BaseMapper<Order> { // 无需额外方法 }关键点:
- 使用逻辑表名而非物理表名
- 主键类型指定为ASSIGN_ID
- Mapper保持标准Mybatis-Plus写法
3.2 复杂查询的处理方案
场景一:按用户分页查询订单
public Page<Order> queryUserOrders(Long userId, int page, int size) { return orderMapper.selectPage( new Page<>(page, size), new LambdaQueryWrapper<Order>() .eq(Order::getUserId, userId) .orderByDesc(Order::getCreateTime) ); }场景二:跨分片统计(需特别注意)
// 不推荐写法(性能极差) @Select("SELECT SUM(amount) FROM t_order") BigDecimal totalAmount(); // 推荐方案:使用ShardingSphere的归并查询 public BigDecimal getTotalAmount() { return orderMapper.selectList(new QueryWrapper<Order>() .select("SUM(amount) as total")) .stream() .map(o -> o.getTotal()) .findFirst() .orElse(BigDecimal.ZERO); }4. 生产环境踩坑实录
4.1 连接池配置优化
spring: shardingsphere: datasource: ds0: type: com.alibaba.druid.pool.DruidDataSource initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000常见问题:
- 连接泄漏导致池耗尽
- 分库后连接数需求翻倍
- 长事务阻塞连接释放
4.2 分布式事务处理
对于订单创建→扣减库存的场景:
@ShardingTransactionType(TransactionType.XA) @Transactional public void createOrder(OrderDTO dto) { // 1. 创建订单 orderMapper.insert(convertToOrder(dto)); // 2. 扣减库存 stockService.reduce(dto.getSkuId(), dto.getQuantity()); // 3. 其他业务操作... }支持的事务类型:
- XA(强一致,性能较低)
- BASE(最终一致,推荐)
4.3 监控与调优要点
关键监控指标:
| 指标项 | 预警阈值 | 排查方向 |
|---|---|---|
| SQL执行耗时 | >500ms | 慢查询、索引缺失 |
| 连接池活跃数 | >80% maxActive | 连接泄漏、事务未关闭 |
| 分片命中率 | <90% | 分片键使用不当 |
5. 进阶:弹性扩容方案
当现有分片不够用时,我们采用以下扩容流程:
- 预分片设计:初始按4库×4表设计,实际只部署2库×2表
- 数据迁移:使用ShardingSphere-Scaling进行在线迁移
- 流量切换:通过配置中心动态更新分片规则
扩容期间的配置示例:
# 旧规则(2库×2表) actual-data-nodes: ds$->{0..1}.order_$->{0..1} # 新规则(4库×4表) actual-data-nodes: ds$->{0..3}.order_$->{0..3}这种方案可以实现:
- 业务无感知扩容
- 分钟级完成数据迁移
- 支持回滚机制
在最近一次大促前,我们通过这套方案将系统吞吐量提升了300%,平均响应时间保持在200ms以内。实际开发中最大的体会是:与其在业务代码中硬编码分片逻辑,不如将这类基础设施问题交给专业中间件处理。