MyBatis-Plus 动态分表实战:基于 DynamicTableNameInnerInterceptor 的多端数据隔离方案
一、概述
本文基于MyBatis-PlusDynamicTableNameInnerInterceptor实现动态分表,核心思路是:在 SQL 执行前,通过 ThreadLocal 传递表名后缀,拦截器自动将逻辑表名替换为物理表名,从而实现多端数据隔离(如端 At_product_a、端 Bt_product_b)。
二、核心类职责
| 类 | 职责 |
|---|---|
ShardInterceptorConfig | 配置类,注册拦截器并绑定 Handler |
TableNameShardHandler | 拦截器回调,执行表名拼接 |
ShardContextHelper | ThreadLocal 工具类,存取/清除后缀 |
ShardTableProperties | 配置属性,定义默认后缀值 |
以上类名均为示意,实际项目中按自身命名规范调整。
三、调用链路
业务方法 → setShardType(type) → ShardContextHelper.setShardSuffix(suffix) ↓ (ThreadLocal 存储) MyBatis 执行 SQL ↓ DynamicTableNameInnerInterceptor 拦截 ↓ TableNameShardHandler.dynamicTableName(sql, tableName) ↓ ShardContextHelper.getShardSuffix() ↓ tableName + "_" + suffix → 实际物理表名 ↓ 业务方法 finally → clearShardType()四、各层详细逻辑
4.1 配置层 —ShardInterceptorConfig
@Bean("shardingMybatisPlusInterceptor")publicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();DynamicTableNameInnerInterceptordynamicTableNameInnerInterceptor=newDynamicTableNameInnerInterceptor();dynamicTableNameInnerInterceptor.setTableNameHandler(newTableNameShardHandler());interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);returninterceptor;}- 创建
DynamicTableNameInnerInterceptor,设置全局唯一的TableNameShardHandler - 该拦截器会拦截所有 SQL,对每张表都调用 Handler
4.2 拦截层 —TableNameShardHandler
@OverridepublicStringdynamicTableName(Stringsql,StringtableName){returntableShard(tableName);}publicStringtableShard(StringtableName){Stringsuffix=ShardContextHelper.getShardSuffix();if(ObjectUtil.isNotEmpty(suffix)){returntableName+"_"+suffix;}returntableName;}- 从 ThreadLocal 读取后缀,非空则拼接
原表名_后缀 - 后缀为空时原样返回,即不设置后缀的查询走原表
4.3 上下文传递层 —ShardContextHelper
privatestaticThreadLocal<String>shardSuffix=newThreadLocal<>();publicstaticvoidsetShardSuffix(Stringsuffix)// 设置publicstaticStringgetShardSuffix()// 获取publicstaticvoidclearShardSuffix()// 清除- 使用
ThreadLocal保证线程隔离,不同请求可路由到不同分表 - 提供 set/get/clear 三段式操作
4.4 配置属性层 —ShardTableProperties
privateStringtableTypes="a,b";// 支持的分表类型privateStringdefaultType="a";// 默认后缀defaultType:默认后缀为"a",即未指定 type 时默认查端 A 分表tableTypes:声明了支持的分表类型列表
五、业务使用模式
所有业务类遵循统一的try-finally 模式:
publicList<XxxResponse>someMethod(Stringtype){try{// 1. 确定后缀:未传则用默认值if(StringUtils.isBlank(type)){type=shardTableProperties.getDefaultType();}// 2. 设置 ThreadLocalsetShardType(type);// 3. 执行数据库操作(此时 SQL 中的表名已被拦截器替换)...}finally{// 4. 清除 ThreadLocal,防止线程复用导致污染clearShardType();}}适用场景示例
| 场景 | 分表用途 |
|---|---|
| 运营内容管理 | 不同端的内容隔离(tabs/files/guides) |
| 行业列表 | 按端隔离行业数据 |
| 产品列表 | 按端隔离产品数据 |
| 资源文件 | 按端隔离文件资源 |
| 收藏功能 | 按端隔离用户收藏 |
六、分表效果示例
假设逻辑表名为t_product:
| type 参数 | ThreadLocal 后缀 | 实际查询表 |
|---|---|---|
"a" | a | t_product_a |
"b" | b | t_product_b |
null/ 空 | a(默认) | t_product_a |
七、设计特点
- 线程安全:基于 ThreadLocal,每个请求线程独立,互不干扰
- 对业务代码侵入小:只需在业务方法前后 set/clear,Mapper 层无感知
- 灵活路由:通过请求参数动态决定查哪张分表,支持多端数据隔离
- 防御性编程:finally 块确保 ThreadLocal 清除,避免线程池复用污染
八、潜在问题
8.1 全局拦截无差别替换
DynamicTableNameInnerInterceptor会对所有表执行 Handler,包括不需要分表的表。当 ThreadLocal 有值时,所有表名都会被拼接后缀,若某张表不存在对应的分表则会报错。当前依赖"不设后缀则不替换"来规避,但一旦忘记 clear 或在嵌套调用中设置,可能误伤其他表。
8.2 异步线程丢失上下文
如果业务方法中使用了CompletableFuture异步并发查询,子线程无法继承父线程的 ThreadLocal 值。若主线程未设置分表后缀就提交了异步任务,子线程中的查询可能走错表或走原表。
8.3 set/clear 重复散落
每个业务类都重复编写了setShardType()/clearShardType()方法,且 try-finally 模板代码大量重复,可抽取为模板方法或 AOP 切面统一管理。
8.4 命名规范
方法名中 “Shard” 拼写需统一(Shard = 分片),避免 “Shading”(阴影)等拼写错误混入。