SpringBoot 2.7.18 配 ShardingSphere-JDBC 做按月分表,这套组合我前后调了一周才彻底跑顺,中间踩了不少坑。今天把完整方案和排障过程整理出来,项目用的是 PostgreSQL 15 + Druid 1.2.20 + MyBatis-Plus 3.5.3,分表维度选的业务表中的创建时间字段。如果你也在折腾类似的需求,这篇可以直接照着落地。
我要先说明一下,很多文章喜欢贴一堆配置然后说"就这样完成了",实际根本不是那么回事。分表方案落地最大的难点在分片算法的编写、分片键的选择、以及 ShardingSphere 与连接池、ORM 框架之间的兼容性配置。我先讲整体设计思路,再给完整的可运行配置,最后把我在真实项目中遇到的问题逐个列出来,这些都是文档里查不到的东西。
先聊聊为什么最终选了 ShardingSphere-JDBC 而不是其他方案,以及这套技术栈搭配的底层逻辑。
1. 项目背景与整体思路
1.1 为什么需要按月分表
业务系统跑了一段时间后,单表数据量很容易突破千万级甚至上亿。拿我手上的订单系统举例,一个季度订单量就到三千万,单表查询的索引深度、写入锁竞争、以及统计类 SQL 的扫描成本都会明显上升。即使 PostgreSQL 的 MVCC 机制和索引能力很强,单表过亿之后,日常业务的响应时间会从几十毫秒逐渐退化到几百毫秒,DBA 那边也会频繁告警。
分表的核心诉求是控制单表数据量。按月分表的好处在于数据边界清晰:历史月份的数据是只读的,当前月份的数据持续写入,归档和清理也方便——直接 drop 掉三个月前的物理表就行,不需要复杂的 delete 操作。而且大部分业务查询都天然带时间范围条件,比如"查这个月的订单""查某天的流水",分片键和时间条件能对上,路由效率就很高。
1.2 技术选型与版本搭配
分库分表中间件主流的有 ShardingSphere、MyCat,还有一些自研的数据库代理方案。ShardingSphere 家族里我选的是 JDBC 模式,而不是 Proxy 模式,原因很现实:JDBC 模式以 jar 包形式嵌入应用进程,不需要额外部署中间件节点,对现有架构的侵入最小,运维成本也低。它直接拦截应用发出的 SQL,根据配置的路由规则改写并路由到真实表,然后合并结果集返回给应用。Proxy 模式虽然支持多语言、能做到集中管理,但要维护独立服务,还要考虑高可用,对小团队来说有点重。
SpringBoot 版本我锁定 2.7.18,这是 Spring Boot 2.x 的最后一个维护版本,稳定性有保障。ShardingSphere-JDBC 版本选 5.2.1,这里要特别注意:5.x 系列的 API 和 4.x 差别很大,网上很多旧文章用的是 4.x 的配置方式,直接照搬会报错。Druid 用 1.2.20,MyBatis-Plus 用 3.5.3,这几个版本组合在一起是经过我实际验证的,可以放心用。
补充一点:如果你用 Spring Boot 3.x 或者 JDK 17 以上,ShardingSphere 5.2.1 的兼容性会很麻烦(需要单独适配 Jakarta API),项目没特殊要求就老老实实用 JDK 8 + Spring Boot 2.7.x。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心配置与分片算法实现
2.1 依赖引入与建表脚本
先加 Maven 依赖。这里有个容易踩的坑:shardingsphere-jdbc-core-spring-boot-starter 这个包会自动引入一个 ShardingSphere 的数据源 Bean,会和 Druid 的自动配置打架,所以要把 Druid 的 starter 排除掉部分自动配置,或者调整 Bean 的注入方式。
xml复制<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
注意:ShardingSphere 的 starter 中自带了一个 spring.factories 里面的数据源自动配置类,它期望你自己提供真正的物理数据源配置。需要在 application.yml 里显式配置 spring.shardingsphere.datasource.names,把 Druid 作为底层数据源注册进去,否则启动时会报数据源找不到。
以订单表为例,物理表设计如下:
sql复制CREATE TABLE t_order_202501 (
id BIGINT PRIMARY KEY,
order_no VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
order_amount NUMERIC(10,2) NOT NULL DEFAULT 0,
create_time TIMESTAMP NOT NULL,
update_time TIMESTAMP NOT NULL
);
CREATE INDEX idx_order_user_202501 ON t_order_202501(user_id);
CREATE INDEX idx_order_create_202501 ON t_order_202501(create_time);
物理表按月建好,逻辑表名统一用 t_order。ShardingSphere 的作用就是让应用只感知 t_order,底层自动路由到 t_order_202501、t_order_202502 这些表。
2.2 application.yml 配置拆解
Spring Boot 配置这块是重点,我直接把可用的配置贴出来,然后逐项说明含义。
yaml复制spring:
shardingsphere:
datasource:
names: ds
ds:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/order_db
username: postgres
password: 123456
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds.t_order_$->{2025..2026}${(1..12).collect{it.toString().padLeft(2,'0')}}
table-strategy:
standard:
sharding-column: create_time
sharding-algorithm-name: order_month_algorithm
key-generate-strategy:
column: id
key-generator-name: snowflake
sharding-algorithms:
order_month_algorithm:
type: CLASS_BASED
props:
strategy: standard
algorithmClassName: com.example.sharding.MonthShardingAlgorithm
key-generators:
snowflake:
type: SNOWFLAKE
props:
sql-show: true
这里解释几个关键点:
actual-data-nodes 用了表达式 ds.t_order_$->{2025..2026}${(1..12).collect{it.toString().padLeft(2,'0')}},含义是生成 t_order_202501 到 t_order_202612 一共 24 张物理表的路由范围。$->{} 是 Groovy 表达式语法,ShardingSphere 用它来做位补零和范围展开。如果你不想每次改年份都去改配置文件,可以拆出来用 actualDataNodes 动态获取,后面我会提到一种通过自定义 DynamicTableName 的方式。
table-strategy.standard.sharding-column: create_time 表示分片键是 create_time 字段,等值查询和范围查询都会走这里配置的算法。CLASS_BASED 类型允许我们指定一个自定义算法类,比内置的 INTERVAL 算法更灵活。
key-generate-strategy 里配置了雪花算法生成主键,分布式场景下避免多个节点插入时主键冲突。注意 ShardingSphere 的雪花算法生成的主键默认是 Long 类型,在 MySQL 端没问题,但 PostgreSQL 端如果要存 BIGINT 也要确保实体类的 id 是 Long,不能是 Integer。
Druid 的配置直接嵌套在 spring.shardingsphere.datasource.ds.druid 下面,用的是 druid-spring-boot-starter 的配置项。连接池参数根据自己的并发压力量调节,我这里是最小 5、最大 20。
props.sql-show: true 打开 SQL 解析日志,调试阶段一定要开着。它能让你看到原始 SQL 被改写成什么样子、路由到哪几张表,排查问题基本靠它。
2.3 分片算法代码的写法与测试
自定义分片算法是实现按月分表的核心,分成等值路由和范围路由两部分。
ShardingSphere 5.2.1 的标准分片接口是 StandardShardingAlgorithm<T>,需要实现 doSharding 方法。等值分片和范围分片在同一个类里通过 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个接口体现,但实际开发时建议直接继承 StandardShardingAlgorithm,它同时要求实现这两类方法。
java复制package com.example.sharding;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
public class MonthShardingAlgorithm implements StandardShardingAlgorithm<LocalDateTime> {
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyyMM");
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<LocalDateTime> shardingValue) {
LocalDateTime createTime = shardingValue.getValue();
String suffix = createTime.format(MONTH_FORMAT);
for (String targetName : availableTargetNames) {
if (targetName.endsWith(suffix)) {
return targetName;
}
}
throw new IllegalArgumentException("未找到匹配的分表: " + targetName);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<LocalDateTime> shardingValue) {
LocalDateTime start = shardingValue.getValueRange().lowerEndpoint();
LocalDateTime end = shardingValue.getValueRange().upperEndpoint();
Collection<String> result = new java.util.LinkedHashSet<>();
for (String targetName : availableTargetNames) {
String suffix = targetName.substring(targetName.length() - 6);
LocalDateTime month = LocalDateTime.parse(suffix + "01", DateTimeFormatter.ofPattern("yyyyMMdd"));
// 判断该物理表对应的月份是否与查询范围有交集
if (!month.isAfter(end.withDayOfMonth(end.getMonth().lengthOfMonth()))
&& !month.plusMonths(1).isBefore(start.withDayOfMonth(1))) {
result.add(targetName);
}
}
return result;
}
@Override
public String getType() {
return "CLASS_BASED";
}
}
这段代码要做细说一下:
doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) 处理的是等值查询,比如 WHERE create_time = '2025-03-15 12:00:00'。方法从分片值中提取月份,拼出后缀 202503,然后遍历可用物理表名,后缀匹配就返回。注意这个方法返回值只有一个,因为等值条件只可能命中的一张物理表。
doSharding(Collection availableTargetNames, RangeShardingValue shardingValue) 处理的是范围查询,比如 WHERE create_time BETWEEN '2025-01-01' AND '2025-03-01',或者 >、<、IN 范围条件。这里要遍历所有物理表,判断每张表代表的月份范围是否和查询区间重叠。我用了一个取巧的判断方式:把物理表的 yyyyMM 后缀当作当月 1 号,判断当月最后一天是否大于等于查询区间起始日,以及下个月 1 号是否小于等于查询区间结束日。只要两个条件都满足就加入结果集。
测试时我建议单独写个 main 方法或者单测来验证路由结果,不用启动整个应用。比如:
java复制public static void main(String[] args) {
MonthShardingAlgorithm algorithm = new MonthShardingAlgorithm();
List<String> tables = java.util.Arrays.asList("t_order_202501", "t_order_202502", "t_order_202503");
String result = algorithm.doSharding(tables, new PreciseShardingValue<>("t_order", "create_time",
LocalDateTime.parse("2025-03-15T10:00:00")));
System.out.println(result); // 期望输出 t_order_202503
}
这个验证成本很低,发现问题能马上定位是算法问题还是配置问题。
3. 业务代码与实战要点
3.1 实体类与 Mapper 层的写法
分表对业务代码的侵入其实很小。实体类只需要把 @TableName 注解指向逻辑表名,然后分片键 create_time 要正常映射。
java复制package com.example.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("t_order")
public class OrderEntity {
@TableId(type = IdType.INPUT)
private Long id;
private String orderNo;
private Long userId;
private BigDecimal orderAmount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
这里有个关键点:因为主键用 ShardingSphere 的雪花算法生成,实体类里主键的 @TableId 要设置成 IdType.INPUT,不依赖数据库自增,也不让 MyBatis-Plus 自动生成。如果配成 AUTO,MyBatis-Plus 会在插入时尝试获取数据库自增 ID,和 ShardingSphere 的生成策略冲突。
Mapper 层直接用 MyBatis-Plus 的 BaseMapper,不需要写分表的任何代码:
java复制package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.OrderEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends BaseMapper<OrderEntity> {
}
用 MyBatis-Plus 自带的 insert、selectById、selectPage 都能正常路由,因为分片键 create_time 在实体映射里是明确存在的,MP 生成 SQL 时会带上这个字段。
3.2 Service 层写好分页、插入与事务管理
写 Service 时最需要注意的是:MyBatis-Plus 的分页插件需要单独配置分页拦截器,而且要和 ShardingSphere 的 SQL 改写兼容。在 ShardingSphere + MyBatis-Plus 组合下,分页插件的 PaginationInnerInterceptor 要设置在 MybatisPlusInterceptor 中,并指定数据库类型为 postgresql。
java复制package com.example.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.POSTGRE_SQL);
paginationInterceptor.setOverflow(false);
paginationInterceptor.setMaxLimit(500L);
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
Service 层的插入逻辑可以直接 save,也可以批量 saveBatch。但我要提醒一下:ShardingSphere 5.x 对批量插入的 SQL 改写支持得还可以,saveBatch 会把批量插入拆成多条单表插入语句,在实际使用中是能用的。不过性能上,实测下来 saveBatch 1000 条数据大约比逐条 save 快 5 倍左右,但和原生 PG 批量 insert 相比还是慢不少,因为每条 SQL 都要走解析-改写-路由-执行全流程。如果你对写入性能有极致要求,建议用 JdbcTemplate 直接执行批量插入,绕过 MyBatis-Plus 的层层包装。
分页查询时要特别注意:ShardingSphere 在合并多表结果集时,如果 SQL 里有 ORDER BY 和 LIMIT,它需要把每个分片的结果都查出来再内存归并,所以跨月的分页查询性能上限不高。我建议业务查询尽量限制在单月或者两三个月范围内,这样路由到的物理表少,性能才可控。
写代码时的几个细节:
- 所有 SQL 都要带上
create_time条件,否则 ShardingSphere 无法路由,会报"Sharding value must be provided"之类的错误。MyBatis-Plus 的selectById不带分片键,只有主键,这种情况会直接报错。正确的做法是先查出create_time或把 ID 改成带时间信息的组合ID。 - 事务里涉及跨分片写操作(比如同时写 t_order_202501 和 t_order_202502),默认的本地事务无法保证原子性。需要引入 ShardingSphere 的
@ShardingSphereTransactionType(TransactionType.XA)注解,配合narayana或者atomikos提供 XA 事务管理器。这块配置相对复杂,如果不是强一致场景,建议通过业务补偿来规避。 - 查询条件如果对
create_time用了函数,比如DATE_TRUNC('month', create_time),ShardingSphere 的解析器可能识别不出分片键从而无法路由。最好把函数作用在参数上,而不是字段上。
3.3 定时任务自动建表
按月分表之后,DBA 不可能每月凌晨手工建表,代码里要做自动建表的兜底。
我用的是 Spring 自带的 @Scheduled + JdbcTemplate,每月 1 号 00:05 执行一次建表脚本,扫描下个月是否存在物理表,不存在就创建。为什么用 00:05 而不是 00:00?留出 5 分钟的时间差,避免数据库时间同步和应用服务器时钟偏差导致的一瞬间创建失败。
java复制package com.example.job;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Component
@RequiredArgsConstructor
public class TableCreateJob {
private final JdbcTemplate jdbcTemplate;
@Scheduled(cron = "0 5 0 1 * ?")
public void createNextMonthTable() {
String nextMonth = LocalDate.now().plusMonths(1).format(DateTimeFormatter.ofPattern("yyyyMM"));
String tableName = "t_order_" + nextMonth;
String checkSql = "SELECT to_regclass('public." + tableName + "') IS NOT NULL";
Boolean exists = jdbcTemplate.queryForObject(checkSql, Boolean.class);
if (Boolean.TRUE.equals(exists)) {
return;
}
String ddl = "CREATE TABLE " + tableName + " (LIKE t_order_202501 INCLUDING ALL)";
jdbcTemplate.execute(ddl);
// 如果有需要,可以在这里补充注释或索引
}
}
这里用了 PostgreSQL 的 CREATE TABLE ... (LIKE ... INCLUDING ALL) 语法,可以快速复制表结构和约束、索引等,比手写一堆 DDL 语句方便。不过要注意,LIKE INCLUDING ALL 不会复制外键和触发器,外键在多表分片场景下本来就不建议有,所以问题不大。
还有一点:建表任务必须确保在业务流量进来之前完成,建议定时任务加 @SchedulerLock 之类的分布式锁,防止多实例部署时多个节点同时执行建表 SQL 导致冲突。或者简单点,在 DDL 里加 IF NOT EXISTS,避免重复执行报错。
4. 常见问题与排查技巧
4.1 分片键不生效导致全路由
最典型的报错是:Sharding value must be provided。原因通常是 SQL 的 WHERE 条件里没有分片键。比如 MyBatis-Plus 的 selectById 生成的 SQL 是 SELECT * FROM t_order WHERE id = ?,没有 create_time,所以无法路由。解决办法参考前面的建议:不在主键上做文章,而是把查询条件补充分片键。
另外还有一种情况是 SQL 里明明有 create_time 字段,但类型不匹配导致路由失败。比如参数传的是 String 类型的 "2025-01-15 10:00:00",而分片键在实体里是 LocalDateTime。ShardingSphere 的字符串日期比较时会截取,可能导致匹配不到物理表。建议接口入参直接用 LocalDateTime,或者统一转成 LocalDateTime 再进入 DAO 层。
4.2 日期处理与时区路由错位
PostgreSQL 的 TIMESTAMP WITH TIME ZONE 和 TIMESTAMP 是两种类型。如果创建时间是 TIMESTAMP WITH TIME ZONE,应用层用 LocalDateTime 接收,JDBC 驱动返回的是带时区的值,会导致时间偏移。这是最容易踩的暗坑:白天插入的数据,到了晚上查询,路由到的物理表可能是上个月的。
排查方法很简单:打开 sql-show,看实际执行 SQL 里传给分片算法的 create_time 值是多少,和北京时间是不是差了几个小时。如果是这个问题,统一数据库字段类型为 TIMESTAMP,或者应用连接串加 serverTimezone=Asia/Shanghai 参数,两边对齐就行。
4.3 Druid 与 ShardingSphere 的兼容性细节
Druid 是业界常用的连接池,但它自带 SQL 防火墙和监控过滤器,这些过滤器可能会拦截或改写 ShardingSphere 拦截器解析过的 SQL,导致路由不准。我在调试时遇到过 druid.sql.Statement.executeQuery 报语法错误,但直接执行同样 SQL 却没问题的情况,就是防火墙和 ShardingSphere 的解析器冲突。
解决方案有两种:一是把 Druid 的 proxy-filters 全部关掉,只保留基础连接池功能;二是把 Druid 配置为普通数据源(不使用 druid-spring-boot-starter 的自动配置),让 ShardingSphere 自己管理数据源包装。我在生产环境用的是第二种,更干净:
java复制@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:postgresql://127.0.0.1:5432/order_db");
dataSource.setUsername("postgres");
dataSource.setPassword("123456");
dataSource.setInitialSize(5);
dataSource.setMinIdle(5);
dataSource.setMaxActive(20);
return dataSource;
}
}
然后 application.yml 里不再配置 Druid 参数,ShardingSphere 的 ds 数据源类型可以保持 DruidDataSource,但它会通过 Spring 容器拿到你手动创建的 DataSource 实例。这种方式绕开了自动配置冲突,排查问题时也更可控。
4.4 跨月查询性能优化
跨月查询分页时,ShardingSphere 会先把每个分片查出的结果全部加载到内存,再排序,最后做全局分页。如果路由到 6 张物理表,每张表都查出 20 条数据,总共 120 条,内存做归并排序是没问题的。但如果你用 LIMIT 100000, 20 这种深分页,ShardingSphere 会把每张物理表的 100020 条记录都加载出来,内存和 IO 成本瞬间爆炸。
避坑经验:
- 深分页场景不要直接翻页,改用"按时间游标"的方式。即记录上一页最后一条记录的
create_time和id,下一页查询用WHERE (create_time, id) < (?, ?)来定位,这样每次 LIMIT 的量都不大。 - 明确月份范围查询时,从业务层限制最大跨度。举个例子,报表查询如果用户选了半年跨度,可以提示"最多支持 3 个月"。
- 如果跨月查询是常态,考虑在上层加一层汇总表,或者用 PostgreSQL 的物化视图按天预聚合,别把所有查询都压到分片上。
- 针对
ORDER BY create_time DESC LIMIT 20这种"最近 N 条"的查询,在每张物理表的create_time字段上加索引,让各分片先返回 20 条,ShardingSphere 内存归并时数据量很小,性能表现不错。
4.5 启动常见报错速查
我把调试过程中遇到的启动报错和解决方案整理了一下,给各位参考:
| 报错信息 | 常见原因 | 解决方式 |
|---|---|---|
Cannot resolve DataSource |
ShardingSphere 没有拿到正确的数据源配置 | 检查 spring.shardingsphere.datasource.names 是否匹配,物理数据源是否正常注入 |
Sharding value must be provided |
SQL 中缺少分片键 | 查询条件必须包含 create_time 字段 |
Can not find sharding rule with name |
sharding-algorithm-name 配置的算法名和 sharding-algorithms 下定义的名字不一致 |
核对名称,区分大小写 |
Postgresql do not support ... |
ShardingSphere 5.2.1 对某些 PG 特有语法识别有限 | 改写 SQL,避免使用 ON CONFLICT 等高级语法 |
java.sql.SQLException: Protocol violation |
连接池和 ShardingSphere 的包装层级冲突 | 手动创建 DataSource 并显式提供连接池参数,避免自动配置 |
Caused by: groovy.lang.MissingMethodException |
actual-data-nodes 表达式 Groovy 语法错误 |
检查 $->{} 表达式中是否用了不支持的语法,可以直接写死真实表名测试 |
4.6 几个容易忽略的细节
再补充几个实际开发中容易忽略的点。
第一,CREATE TABLE ... (LIKE ... INCLUDING ALL) 建的表会继承默认值约束,但 PG 的 serial 序列不会自动复制。如果业务里用了自增序列,建表后要手动设置序列。
第二,ShardingSphere 5.2.1 对 PostgreSQL 的 RETURNING 子句支持有限。MyBatis-Plus 的 save 走的是标准 insert,倒是没遇到问题,但如果你写原生 SQL 用了 INSERT ... RETURNING id,可能会报错或者返回结果不对。建议走稳妥的方案,主键由应用层生成,不管数据库返回。
第三,sql-show 打开后日志会很多,生产环境建议关掉,否则多一个解析层会拖慢整体吞吐。实测开启 sql-show 的 TPS 大约是关闭状态下的 80% 左右,如果对性能敏感,调试完记得改回 false。
5. 这套方案的后续可扩展方向
分表做完之后,有些场景还可以继续演进。
数据一致性方面,跨分片的分布式事务可以看 ShardingSphere 的 XA 实现,或者更轻量地用本地消息表加最终一致性方案。如果你的业务允许短暂不一致,其实可以不引入分布式事务,应用层做幂等补偿就够了。
查询能力方面,路由到单个月份的查询很快,但跨月聚合的报表查询可能仍然慢。我之前在项目里加了一张家日报表汇总表,按天统计订单数量、金额、用户数等指标,每天凌晨跑一次批量任务写入。查询报表时只查汇总表,不碰按月分片的大表,响应时间能保持在百毫秒以内。
数据保留和归档方面,按月分表天然适合滚动删除。可以写一个定时任务,在每天凌晨扫描是否存在超过 N 个月的物理表,直接 DROP TABLE 或者先迁移到历史库再删除。这样的好处是不需要写复杂的 delete 条件,也不会产生大量 WAL 日志。
我个人的体会是:分表方案的核心不只是把配置写好,更重要的是理解路由的边界条件。数据进了分片之后,很多常规的 SQL 写法就需要规避——比如不带分片键的单点查询、跨多月的深分页、依赖数据库自增主键的插入逻辑。把这些问题在开发阶段就同步给团队,约定好 DAO 层的方法规范,会省掉很多后期排查的时间。
这套 SpringBoot 2.7.18 + ShardingSphere-JDBC 5.2.1 的配置,如果你也是 PostgreSQL + Druid + MyBatis-Plus 的组合,按上面的步骤走一遍基本能跑通。真遇到问题,开着 sql-show 看日志,定位到路由层面,一般都能找到原因。
