ShardingJDBC 分库分表接入记录
在评估过 MyCat2 之后,因为 SQL 兼容性问题(跨分片 JOIN、复杂子查询)最终选了 ShardingJDBC。详细选型对比可以看上一篇:MyCat2 使用记录与放弃复盘。
ShardingJDBC(现在叫 ShardingSphere JDBC 模式)以 JAR 包集成进应用,在 JDBC 层拦截 SQL,对 SQL 做解析、改写和路由。相比代理模式少一跳网络,性能更好,SQL 兼容性也更强。
一、环境与版本
- ShardingSphere 版本:5.4.1
- Spring Boot 版本:2.7.x
- 数据库:MySQL 8.0,2 个实例,每实例各 2 个 schema(
order_db_0 / order_db_1) - 分片表:
t_order,目标是 2 库 × 4 表 = 8 个物理分片
二、接入步骤
引入依赖
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core</artifactId> <version>5.4.1</version> </dependency>
|
注意:ShardingSphere 5.x 不再提供 sharding-jdbc-spring-boot-starter,改为统一的 shardingsphere-jdbc-core,配置方式也有较大变化。
分片规则配置(YAML 模式)
application.yml 指向 ShardingSphere 配置文件:
spring: datasource: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver url: jdbc:shardingsphere:classpath:shardingsphere.yaml
|
resources/shardingsphere.yaml 完整配置:
dataSources: ds0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource jdbcUrl: jdbc:mysql://127.0.0.1:3306/order_db_0?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 connectionTimeout: 30000 maximumPoolSize: 20 ds1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource jdbcUrl: jdbc:mysql://127.0.0.1:3307/order_db_1?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 maximumPoolSize: 20
rules: - !SHARDING tables: t_order: actualDataNodes: ds${0..1}.t_order_${0..3} databaseStrategy: standard: shardingColumn: user_id shardingAlgorithmName: db-inline tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: table-inline keyGenerateStrategy: column: order_id keyGeneratorName: snowflake
shardingAlgorithms: db-inline: type: INLINE props: algorithm-expression: ds${user_id % 2} table-inline: type: INLINE props: algorithm-expression: t_order_${order_id % 4}
keyGenerators: snowflake: type: SNOWFLAKE props: worker-id: 1
|
三、自定义分片算法
内置的 INLINE(取模)、MOD(取模)、HASH_MOD(哈希取模)不够用的场景:
- 按时间范围分片(每月一张表,方便归档历史数据)
- 按租户 ID 分片(不同租户隔离到不同库)
示例:按月份分表
public class MonthShardingAlgorithm implements StandardShardingAlgorithm<Date> {
@Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) { Date orderTime = shardingValue.getValue(); String suffix = new SimpleDateFormat("yyyyMM").format(orderTime); String targetTable = shardingValue.getLogicTableName() + "_" + suffix; if (availableTargetNames.contains(targetTable)) { return targetTable; } throw new IllegalArgumentException("目标表 " + targetTable + " 不存在,请先创建"); }
@Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) { List<String> result = new ArrayList<>(); Range<Date> range = shardingValue.getValueRange(); Date start = range.lowerEndpoint(); Date end = range.upperEndpoint(); Calendar cal = Calendar.getInstance(); cal.setTime(start); while (!cal.getTime().after(end)) { String suffix = new SimpleDateFormat("yyyyMM").format(cal.getTime()); String tableName = shardingValue.getLogicTableName() + "_" + suffix; if (availableTargetNames.contains(tableName)) { result.add(tableName); } cal.add(Calendar.MONTH, 1); } return result; }
@Override public String getType() { return "MONTH"; } }
|
shardingAlgorithms: month-algo: type: MONTH
|
四、结合多数据源使用
项目里分片表(订单、流水)用 ShardingSphere,非分片表(用户、配置、日志)用普通数据源。两者通过 dynamic-datasource 共存:
spring: datasource: dynamic: primary: master datasource: master: url: jdbc:mysql://127.0.0.1:3306/user_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver sharding: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver url: jdbc:shardingsphere:classpath:shardingsphere.yaml
|
@DS("sharding") public void createOrder(Order order) { orderMapper.insert(order); }
public User getUser(Long userId) { return userMapper.selectById(userId); }
|
五、SQL 限制清单
ShardingSphere 比 MyCat2 的 SQL 兼容性好很多,但还是有一些限制,踩过坑的记录在这里:
| 限制项 | 说明 | 解决方案 |
|---|
UPDATE/DELETE 不带分片键 | 会广播到所有分片,性能问题 | 强制要求 WHERE 带分片键 |
跨分片 DISTINCT | 结果可能有重复(各分片各去重,合并后不去重) | 应用层去重,或改成唯一键过滤 |
| 子查询的分片键与外层不一致 | 路由推断失败,退化为全分片广播 | 改写 SQL,把分片键提到外层 |
跨分片 GROUP BY + HAVING | 在 Executor 层聚合,大数据量时性能差 | 分片内聚合后应用层二次聚合 |
| 不支持存储过程 | 直接报错 | 迁移存储过程逻辑到应用层 |
LIMIT 大偏移量分页 | 同 MyCat2,内存合并问题 | 改用游标分页 |
六、踩坑记录
坑一:workerId 重复导致 ID 冲突
多实例部署时,雪花算法的 worker-id 必须不同。刚上线时两台机器用了同一个配置,同一秒内生成了相同的 order_id,入库报主键冲突。
解决方案:worker-id 改为从环境变量读取,K8s 部署时通过 Pod 的环境变量注入不同的值:
keyGenerators: snowflake: type: SNOWFLAKE props: worker-id: ${WORKER_ID:1}
|
坑二:分片键类型不匹配导致路由错误
数据库分片键是 bigint,应用代码里有个地方传了 String 类型的 user_id(从前端 JSON 解析来的,没有明确指定类型)。ShardingSphere 按 String 类型做路由,和数据库的 bigint 类型不匹配,路由到了错误的分片,数据查不到。
解决方案:在 DTO 上明确 Long 类型,或在转换层做类型转换,确保分片键类型一致。
坑三:事务内的读操作没有强制走主库
启用读写分离后,发现事务内执行写操作后立即读取,读到的是旧数据(从库延迟)。
ShardingSphere 默认在事务内强制走主库,但需要配置 transaction-type 为 LOCAL:
rules: - !READWRITE_SPLITTING dataSources: rw-ds: writeDataSourceName: ds-master readDataSourceNames: - ds-slave transactionalReadQueryStrategy: PRIMARY
|
七、适用边界
什么时候引入分库分表(不要过早引入):
- 单表数据量超过 2000 万行,或预计 2 年内会达到
- 写 QPS 超过单库瓶颈(MySQL 写操作通常在 3000~5000 QPS 时需要考虑)
什么时候分库分表是错误解法:
- 慢查询的根因是索引设计问题,加索引就能解决
- 数据量不大但查询复杂,应该优化 SQL 逻辑而不是分片
- 读多写少的场景,读写分离通常已经够用
一句话:分库分表是最后一招,先把索引优化、缓存、读写分离都用上再考虑它。