ShardingJDBC 分库分表接入记录

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:
# 实际物理节点:ds0.t_order_0 ~ ds0.t_order_3,ds1.t_order_0 ~ ds1.t_order_3
actualDataNodes: ds${0..1}.t_order_${0..3}
# 分库策略:user_id % 2 决定路由到 ds0 还是 ds1
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db-inline
# 分表策略:order_id % 4 决定路由到 t_order_0 ~ t_order_3
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: table-inline
# 分布式主键:使用雪花算法生成 order_id
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 # 多实例部署时每个实例必须不同,否则 ID 冲突

三、自定义分片算法

内置的 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) {
// 范围查询(如 WHERE order_time BETWEEN '2023-01-01' AND '2023-03-31')
// 遍历范围内的所有月份,返回对应的表名列表
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"; // 在 YAML 里通过这个 type 引用
}
}
# SPI 注册:在 META-INF/services/ 下创建文件
# 文件名:org.apache.shardingsphere.sharding.spi.ShardingAlgorithm
# 内容:com.example.sharding.MonthShardingAlgorithm

# YAML 中引用
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:
# ShardingSphere 作为 dynamic-datasource 的一个节点
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:shardingsphere.yaml
# 这里 type 不要配,dynamic-datasource 会自动识别
// 操作分片表时切换到 sharding 数据源
@DS("sharding")
public void createOrder(Order order) {
orderMapper.insert(order);
}

// 操作普通表时用默认的 master 数据源
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} # 从环境变量读,默认 1

坑二:分片键类型不匹配导致路由错误

数据库分片键是 bigint,应用代码里有个地方传了 String 类型的 user_id(从前端 JSON 解析来的,没有明确指定类型)。ShardingSphere 按 String 类型做路由,和数据库的 bigint 类型不匹配,路由到了错误的分片,数据查不到。

解决方案:在 DTO 上明确 Long 类型,或在转换层做类型转换,确保分片键类型一致。

坑三:事务内的读操作没有强制走主库

启用读写分离后,发现事务内执行写操作后立即读取,读到的是旧数据(从库延迟)。

ShardingSphere 默认在事务内强制走主库,但需要配置 transaction-typeLOCAL

rules:
- !READWRITE_SPLITTING
dataSources:
rw-ds:
writeDataSourceName: ds-master
readDataSourceNames:
- ds-slave
transactionalReadQueryStrategy: PRIMARY # 事务内强制走主库

七、适用边界

什么时候引入分库分表(不要过早引入):

  • 单表数据量超过 2000 万行,或预计 2 年内会达到
  • 写 QPS 超过单库瓶颈(MySQL 写操作通常在 3000~5000 QPS 时需要考虑)

什么时候分库分表是错误解法:

  • 慢查询的根因是索引设计问题,加索引就能解决
  • 数据量不大但查询复杂,应该优化 SQL 逻辑而不是分片
  • 读多写少的场景,读写分离通常已经够用

一句话:分库分表是最后一招,先把索引优化、缓存、读写分离都用上再考虑它。