☰
电商系统分库分表实战与ShardingJDBC应用
2026/10/27 12:31:42 网站建设 项目流程

1. 为什么我们需要分库分表?

第一次接触分库分表这个概念是在2015年,当时我负责的一个电商系统遇到了严重的性能瓶颈。数据库服务器CPU长期保持在90%以上,订单表的记录数已经突破3000万,简单的查询都要好几秒才能返回结果。那段时间,每天半夜都会被报警电话叫醒,不是数据库连接池耗尽就是查询超时。

1.1 单库单表的性能天花板

任何单机数据库都有其物理极限。以MySQL为例,当单表数据量超过千万级别时,就会明显出现以下问题:

  • 索引膨胀:B+树深度增加,查询时需要更多的磁盘I/O
  • DDL操作锁表时间过长:添加字段可能需要数小时
  • 备份恢复困难:几百GB的数据备份耗时且风险高
  • 高并发写入瓶颈:单机写入性能受限于磁盘IOPS

我们当时做了一个简单的压测:在一台16核64G的服务器上,MySQL单表在5000万数据量时,TPS已经下降到不足1000。而业务要求的峰值TPS是5000+。

1.2 分库分表的本质

分库分表的核心思想其实很简单:将一个大表按照某种规则(我们称为分片键)拆分成多个小表,分散到不同的数据库实例上。这样每个数据库实例只需要处理部分数据,压力自然就降下来了。

具体来说:

  • 分库:把同一个表的数据拆分到不同的数据库实例
  • 分表:把同一个表的数据拆分到同一个数据库的不同表中

2. ShardingSphere生态与ShardingJDBC

2.1 ShardingSphere全家桶

Apache ShardingSphere是一个开源的分布式数据库中间件生态圈,主要由三个组件组成:

  1. ShardingJDBC:轻量级Java框架,在JDBC层提供分库分表能力
  2. ShardingProxy:透明化的数据库代理,支持所有兼容MySQL协议的客户端
  3. ShardingSidecar(开发中):云原生架构下的数据库治理方案

2.2 为什么选择ShardingJDBC?

在我们的技术选型过程中,主要考虑了以下几点:

  1. 无侵入性:不需要额外部署中间件,应用直连数据库
  2. 性能损耗低:相比Proxy模式,JDBC层分片的性能更好
  3. 灵活度高:支持自定义分片策略,与Spring生态完美集成
  4. 运维成本低:不需要额外维护Proxy服务

实际测试数据显示:ShardingJDBC相比直连数据库的性能损耗在10%以内,而Proxy模式会有20-30%的性能下降。

3. 实战:电商订单系统分库分表改造

3.1 分片策略设计

以电商订单表为例,我们最终选择了"用户ID后两位+月份"的复合分片策略:

spring: shardingsphere: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{202301..202312} database-strategy: inline: algorithm-expression: ds$->{user_id % 2} sharding-column: user_id table-strategy: standard: precise-algorithm-class-name: com.example.MonthPreciseShardingAlgorithm range-algorithm-class-name: com.example.MonthRangeShardingAlgorithm sharding-column: create_time

这个方案考虑了:

  1. 用户维度查询:通过user_id分库保证同一用户的数据在同一个库
  2. 时间维度查询:通过月份分表便于历史数据归档
  3. 扩展性:未来可以很方便地增加数据库实例

3.2 分布式ID生成

分库分表后,自增ID显然不能用了。我们采用了Snowflake算法改造:

public class DistributedIdGenerator { private final long datacenterId; private final long machineId; private long sequence = 0L; private long lastTimestamp = -1L; public synchronized long nextId() { long timestamp = timeGen(); if (timestamp < lastTimestamp) { throw new RuntimeException("Clock moved backwards"); } if (lastTimestamp == timestamp) { sequence = (sequence + 1) & SEQUENCE_MASK; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = timestamp; return ((timestamp - TWEPOCH) << TIMESTAMP_SHIFT) | (datacenterId << DATACENTER_SHIFT) | (machineId << MACHINE_SHIFT) | sequence; } }

3.3 跨库JOIN解决方案

分库后最大的挑战就是跨库JOIN。我们的解决方案是:

  1. 冗余关键字段:在订单表中冗余存储用户昵称等常用信息
  2. 内存计算:先查询主表获取ID,再批量查询关联表
  3. 使用广播表:将地区编码等小表在所有库都保存完整副本
// 示例:先查订单,再批量查用户 List<Long> userIds = orders.stream().map(Order::getUserId).distinct().collect(Collectors.toList()); Map<Long, User> userMap = userService.findByIds(userIds).stream() .collect(Collectors.toMap(User::getId, Function.identity()));

4. 生产环境踩坑实录

4.1 分页查询的陷阱

有一次线上报警,一个分页查询耗时超过10秒。排查发现是有人写了这样的SQL:

SELECT * FROM t_order ORDER BY create_time DESC LIMIT 100000, 10

在分库分表环境下,这个查询会在每个分片上都取100010条数据,然后在内存中合并排序。解决方案:

  1. 使用Elasticsearch做搜索
  2. 限制最大分页深度
  3. 改用游标分页(基于上次查询的最后一条记录)

4.2 分布式事务处理

订单创建涉及多个表更新,我们最终采用了柔性事务方案:

@ShardingTransactionType(TransactionType.BASE) @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { // 主订单表 orderMapper.insert(order); // 订单明细表 order.getItems().forEach(item -> { item.setOrderId(order.getId()); orderItemMapper.insert(item); }); // 库存扣减 inventoryService.reduceStock(order.getItems()); }

BASE事务相比XA性能更好,但需要业务方处理中间状态。我们额外增加了定时任务来补偿异常订单。

5. 性能优化实战技巧

5.1 索引设计原则

分库分表后,索引设计要特别注意:

  1. 每个分片必须有完整索引,不能依赖全局唯一
  2. 分片键必须包含在联合索引中
  3. 避免在非分片键上使用范围查询

好的索引示例:

ALTER TABLE t_order_202301 ADD INDEX idx_user_create (user_id, create_time);

5.2 连接池配置

分库后连接数会成倍增长,需要合理配置:

spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 validation-query: SELECT 1

建议:

  • 每个物理库配置独立的连接池
  • 监控连接使用情况,及时调整参数
  • 使用连接池的过滤器统计SQL执行情况

6. 监控与运维体系建设

6.1 关键监控指标

我们使用Prometheus+Grafana搭建了监控系统,重点关注:

  1. 分片均衡性:各分片数据量差异不超过20%
  2. 慢查询:超过500ms的SQL需要优化
  3. 连接池使用率:超过80%需要扩容
  4. 分布式事务成功率:低于99.9%需要告警

6.2 数据迁移方案

当需要增加分片时,我们采用双写方案:

  1. 配置新旧分片规则同时生效
  2. 存量数据使用DataX工具迁移
  3. 校验数据一致性
  4. 切换读流量到新分片
  5. 逐步下线旧分片

整个过程需要灰度发布,随时可以回滚。

7. 未来架构演进

随着业务发展,我们正在考虑:

  1. 混合分片策略:热数据单独分片
  2. 引入ShardingProxy实现读写分离
  3. 使用分布式时序数据库处理日志类数据
  4. 探索云原生架构下的Service Mesh方案

分库分表不是银弹,它解决了数据量大的问题,但也带来了复杂性。建议在单表超过500万行后再考虑分片,前期可以通过优化索引、升级硬件等方式缓解压力。

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

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

立即咨询