数据分表——使用 Mybatis-Plus插件实现动态表名分表(按年份分表、按月份分表)
本博客适合Mybatis-Plus3.4以上版本,笔者使用版本为3.5.3。
分库与分表的原因
1. 业务场景:日志、交易流水表或者其他数据量大的表,通过日期进行了水平分表,需要通过日期参数,动态的查询数据。
实现思路:利用MybatisPlus的动态表名插件DynamicTableNameInnerInterceptor ,实现Sql执行时,动态的修改表名。
2. 非必须勿使用分库分表:如数据库确实成为性能瓶颈时,在设计分库分表方案时应充分考虑方案的扩展性,或者考虑采用成熟热门的分布式数据库解决方案,如 TiDB。TiDB 数据库,针对 TiKV 中数据的打散,是基于 Range 的方式进行,将不同范围内的[StartKey,EndKey)分配到不同的 Region 上。(目前属实看不懂这最后一句话。。。。)
以上摘自参考博客1
参考博客如下:
参考博客1:mybatis-plus小技能: 分表策略(按年分表和按月分表)
参考博客2:数据分表Mybatis Plus动态表名最优方案的探索
1、分表策略
1.1 在数据库预先创建好按各年份或者月份的分的数据表
1.2 实现动态表名接口
如果动态表名接口在使用时没有赋值,默认操作的是服务器时间当前月份或者年份的表。
1. 实现年份动态表名处理器(YearTableNameHandler)
在你所要使用的SpringBoot工程中创建config配置类包,并在该包下建立handler包用来放置一会儿年份动态表名处理器,起名为YearTableNameHandler.java,使用该类实现TableNameHandler接口并重写dynamicTableName方法。具体模板程序如下:
package com.my.demo17.config.handler;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
/**
* Classname: YearTableNameHandler
* Package: com.my.demo17.config.handler
* Description:
* 按年份参数,组成动态表名
* @Author Alex Liang
* @Create 2024/7/2 下午8:06
* @Version 1.0
*/
public class YearTableNameHandler implements TableNameHandler {
/**
* 用于记录哪些表可以使用该年份动态表名处理器(即哪些表按年份分)
*/
private List<String> tableNames;
//
/**
* 构造函数,构造动态表名处理器的时候,传递tableName参数
* @param tableNames
*/
public YearTableNameHandler(String ...tableNames) {
this.tableNames = Arrays.asList(tableNames);
}
/**
* 每个请求线程维护一个年份数据,避免多线程数据冲突,所以使用ThreadLocal
*/
private static final ThreadLocal<String> YEAR_DATA = new ThreadLocal<>();
/**
* 设置请求线程的年份数据
* @param yearData
*/
public static void setYearData(String yearData) {
YEAR_DATA.set(yearData);
}
/**
* 删除当前请求线程的年份数据
*/
public static void removeYearData(){
YEAR_DATA.remove();
}
/**
* 动态表名接口实现方法
* @param sql
* @param tableName
* @return
*/
@Override
public String dynamicTableName(String sql, String tableName) {
if (this.tableNames.contains(tableName)) {
if (YEAR_DATA.get() == null) {
LocalDate date = LocalDate.now();
return tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyy")); // 表名增加年份后缀
}
return tableName + "_" + YEAR_DATA.get(); // 表名增加年份后缀
}else {
return tableName; // 表名原样返回
}
}
}
2. 实现月份动态表名处理器(YearTableNameHandler)
类似上一步骤在handler包中建立月份动态表名处理器,起名为MonthTableNameHandler.java,使用该类实现TableNameHandler接口并重写dynamicTableName方法。具体模板程序如下:
package com.my.demo17.config.handler;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
/**
* Classname: MonthTableNameHandler
* Package: com.my.demo17.config.handler
* Description:
*
* @Author Alex Liang
* @Create 2024/7/2 下午8:18
* @Version 1.0
*/
public class MonthTableNameHandler implements TableNameHandler {
/**
* 用于记录哪些表可以使用该月份动态表名处理器(即哪些表按月分表)
*/
private List<String> tableNames;
//
/**
* 构造函数,构造动态表名处理器的时候,传递tableName参数
* @param tableNames
*/
public MonthTableNameHandler(String ...tableNames) {
this.tableNames = Arrays.asList(tableNames);
}
/**
* 每个请求线程维护一个月份数据,避免多线程数据冲突,所以使用ThreadLocal
*/
private static final ThreadLocal<String> MONTH_DATA = new ThreadLocal<>();
/**
* 设置请求线程的月份数据
* @param yearData
*/
public static void setMonthData(String yearData) {
MONTH_DATA.set(yearData);
}
/**
* 删除当前请求线程的月份数据
*/
public static void removeMonthData(){
MONTH_DATA.remove();
}
/**
* 动态表名接口实现方法
* @param sql
* @param tableName
* @return
*/
@Override
public String dynamicTableName(String sql, String tableName) {
if (this.tableNames.contains(tableName)) {
if (MONTH_DATA.get() == null) {
LocalDate date = LocalDate.now();
return tableName + "_" + date.format(DateTimeFormatter.ofPattern("MM")); // 表名增加月份后缀
}
return tableName + "_" + MONTH_DATA.get(); // 表名增加月份后缀
}else {
return tableName; // 表名原样返回
}
}
}
1.3 配置类添加动态表名
创建好年份、月份动态表名处理器后,在config包创建MyBatisPlusConfig.java作为MyBatis-Plus类,在该类中我们要添加刚刚写好的年份、月份动态表名处理器,模板代码如下:
package com.my.demo17.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.my.demo17.config.handler.MonthTableNameHandler;
import com.my.demo17.config.handler.YearTableNameHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Classname: MyBatisPlusConfig
* Package: com.my.demo17.config
* Description:
*
* @Author Alex Liang
* @Create 2024/7/2 下午8:03
* @Version 1.0
*/
@Configuration
// @MapperScan("com.my.demo17.mapper") // 如果在运行主类配置过此注解,则不需要再该类中配置此注解
public class MyBatisPlusConfig {
@Bean //令方法创建对象并交给Spring去管理
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
DynamicTableNameInnerInterceptor dynamicTableNameMonthInterceptor = new DynamicTableNameInnerInterceptor();
dynamicTableNameMonthInterceptor.setTableNameHandler(
// 可以传多个表名参数,指定哪些表使用MonthTableNameHandler处理表名称
new MonthTableNameHandler("student")
);
// 以拦截器的方式处理表名称
interceptor.addInnerInterceptor(dynamicTableNameMonthInterceptor);
DynamicTableNameInnerInterceptor dynamicTableNameYearInterceptor = new DynamicTableNameInnerInterceptor();
dynamicTableNameYearInterceptor.setTableNameHandler(
// 可以传多个表名参数,指定哪些表使用YearTableNameHandler处理表名称
new YearTableNameHandler("student")
);
// 以拦截器的方式处理表名称(也可以传递多个拦截器,即:可以传递多个表名处理器TableNameHandler,
// 如以下代码便是在添加月表名拦截器的基础上又加了年表名拦截器)
interceptor.addInnerInterceptor(dynamicTableNameYearInterceptor);
return interceptor;
}
}
1.4 测试
开始测试之前,一定要确保连接的数据库中已经创建了相应的分表,比如我这里测试的分表分别为按年份的student202407和按月份的student07,否则会报错。
package com.my.demo17;
import com.my.demo17.config.handler.MonthTableNameHandler;
import com.my.demo17.config.handler.YearTableNameHandler;
import com.my.demo17.mapper.StudentMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Demo17ApplicationTests {
@Autowired
private StudentMapper studentMapper;
@Test
void contextLoads() {
// 执行之前设置月份(实际场景下,该参数从请求参数中解析)
MonthTableNameHandler.setMonthData("202407");
studentMapper.selectById("01");
MonthTableNameHandler.removeMonthData();
// 执行之前设置月份(实际场景下,该参数从请求参数中解析)
YearTableNameHandler.setYearData("2024");
studentMapper.selectById("01");
YearTableNameHandler.removeYearData();
}
}
测试结果如下: