数据分表——使用 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();

    }

}

测试结果如下:
image

posted @ 2024-07-02 21:26  LiangXin_Alex  阅读(981)  评论(0编辑  收藏  举报