mybatis-plus小技能: 分表策略(按年分表和按月分表)

简介: 业务场景: 日志、交易流水表或者其他数据量大的表,通过日期进行了水平分表,需要通过日期参数,动态的查询数据。实现思路:利用MybatisPlus的动态表名插件DynamicTableNameInnerInterceptor ,实现Sql执行时,动态的修改表名。

引言

业务场景: 日志、交易流水表或者其他数据量大的表,通过日期进行了水平分表,需要通过日期参数,动态的查询数据。
实现思路:利用MybatisPlus的动态表名插件DynamicTableNameInnerInterceptor ,实现Sql执行时,动态的修改表名。

实现步骤:在数据库预先创建好各年份或者月份的表之后,在配置类统一配置拦截器MybatisPlusInterceptor需要处理的动态表。

        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameYearInnerInterceptor = new DynamicTableNameInnerInterceptor();
        // 添加表名处理器
        dynamicTableNameYearInnerInterceptor.setTableNameHandler(
                //可以传多个表名参数,指定哪些表使用YearTableNameHandler处理表名称
                new YearTableNameHandler("t_trans_sub")
        );
        //可以传递多个拦截器,即:可以传递多个表名处理器TableNameHandler
        interceptor.addInnerInterceptor(dynamicTableNameYearInnerInterceptor);

I 分表策略

1.1 配置类添加动态表名

提供按年分表和按月分表的分表策略: 在数据库预先创建好各年份或者月份的表之后,需要分表的到这里配置下。

@Configuration
public class MybatisPlusDynamicTableNameConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        dynamicTableNameInnerInterceptor.setTableNameHandler(
                //可以传多个表名参数,指定哪些表使用MonthTableNameHandler处理表名称
                new MonthTableNameHandler("t_trans_flow","t_trans_flow_act")
        );
        //以拦截器的方式处理表名称
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        DynamicTableNameInnerInterceptor dynamicTableNameYearInnerInterceptor = new DynamicTableNameInnerInterceptor();
        dynamicTableNameYearInnerInterceptor.setTableNameHandler(
                //可以传多个表名参数,指定哪些表使用YearTableNameHandler处理表名称
                new YearTableNameHandler("t_trans_sub")
        );
        //可以传递多个拦截器,即:可以传递多个表名处理器TableNameHandler
        interceptor.addInnerInterceptor(dynamicTableNameYearInnerInterceptor);
        return interceptor;
    }
}

1.2 实现动态表名接口

如果没有赋值的, 默认操作服务器时间当前月份或者年份的表。

  1. 实现年份动态表名处理器(YearTableNameHandler
/**
 * 按年份参数,组成动态表名
 */
public class YearTableNameHandler implements TableNameHandler {

    //用于记录哪些表可以使用该年份动态表名处理器(即哪些表按年分表)
    private List<String> tableNames;
    //构造函数,构造动态表名处理器的时候,传递tableNames参数
    public YearTableNameHandler(String ...tableNames) {
        this.tableNames = Arrays.asList(tableNames);
    }

    //每个请求线程维护一个年份数据,避免多线程数据冲突。所以使用ThreadLocal
    private static final ThreadLocal<String> YEAR_DATA = new ThreadLocal<>();
    //设置请求线程的年份数据
    public static void setData(String month) {
        YEAR_DATA.set(month);
    }
    //删除当前请求线程的年份数据
    public static void removeData() {
        YEAR_DATA.remove();
    }

    //动态表名接口实现方法
    @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;   //表名原样返回
        }
    }
}
  1. 实现月份动态表名处理器(MonthTableNameHandler)

/**
 * 按月份参数,组成动态表名
 */
public class MonthTableNameHandler implements TableNameHandler {

    //用于记录哪些表可以使用该月份动态表名处理器(即哪些表按月分表)
    private List<String> tableNames;
    //构造函数,构造动态表名处理器的时候,传递tableNames参数
    public MonthTableNameHandler(String ...tableNames) {
        this.tableNames = Arrays.asList(tableNames);
    }

    //每个请求线程维护一个month数据,避免多线程数据冲突。所以使用ThreadLocal
    private static final ThreadLocal<String> MONTH_DATA = new ThreadLocal<>();
    //设置请求线程的month数据
    public static void setData(String month) {
        MONTH_DATA.set(month);
    }
    //删除当前请求线程的month数据
    public static void removeData() {
        MONTH_DATA.remove();
    }

    //动态表名接口实现方法
    @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("yyyyMM"));  //表名增加月份后缀
            }
            return tableName + "_" + MONTH_DATA.get();  //表名增加月份后缀
        }else{
            return tableName;   //表名原样返回
        }
    }
}

1.3 用法

//执行数据操作之前设置月份(实际场景下该参数从请求参数中解析)
MonthTableNameHandler.setData("202208");
studentMapper.selectById(1);//以id=1查询student202208这张表
//阅后即焚,将ThreadLocal当前请求线程的数据移除 
MonthTableNameHandler.removeData();

II 分库分表

非必须勿使用分库分表:如数据库确实成为性能瓶颈时,在设计分库分表方案时应充分考虑方案的扩展性,或者考虑采用成熟热门的分布式数据库解决方案,如 TiDB。

TiDB 数据库,针对 TiKV 中数据的打散,是基于 Range 的方式进行,将不同范围内的[StartKey,EndKey)分配到不同的 Region 上。

2.1 什么是分库分表?

分库分表

  • 分表:将一个表中的数据按照某种规则分拆到多张表中,降低锁粒度以及索引树,提升数据查询效率。
水平拆分:将总体数据按照某种维度(时间、用户)分拆到多个库中或者表中。
规则:按照时间划分、按照用户ID划分、按照业务能力划分,如订单按照(日期、用户 ID、区域)分库分表。
  • 分库:将一个数据库中的数据按照某种规则分拆到多个数据库中,以缓解单服务器的压力(CPU、内存、磁盘、IO)。
避免热点数据对于单库单表造成压力,尽可能保证数据流量在各个库表中保持等量分配。

2.2 垂直拆表

大表拆小表: 将一张表中数据的不同”字段“分拆到多张表中,比如商品库将商品基本信息、商品库存、卖家信息等分拆到不同库表中。

将不常用的,数据较大,长度较长(比如 text 类型字段)的字段拆分到“扩展表“,表和表之间通过”主键外键“进行关联。

好处:降低表数据规模,提升查询效率,也避免查询时数据量太大造成的“跨页”问题。

2.3 垂直拆库

  • 垂直拆库:将一个系统中的不同业务场景进行拆分,比如订单表、用户表、商品表。
  • 好处:降低单数据库服务的压力(物理存储、内存、IO 等)、降低单机故障的影响面

2.4 水平拆表

  • 水平拆表:将数据按照某种维度拆分为多张表
  • 好处:降低锁粒度,一定程度提升查询性能。
  • 缺点:由于多张表还是从属于一个库,仍然会有 IO 性能瓶颈。
  • 水平拆分手段:range 分库分表(时间范围分库分表)、hash 分库分表(“主键”进行 hash 来计算数据存储的库表索引)。

时间范围分库分表的缺点:

  • 需要提前建库或表。
  • 数据热点问题:当前时间的数据集中落在某个库表。
  • 分页查询问题:库表中间分界线查询复杂。

2.5 水平拆库

水平拆库:将数据按照某种维度分拆到多个库中
好处:降低单机单库的压力,提升读写性能。

2.6 案例:hash分库分表

  • 独立 hash: 通过主键计算hash值,然后hash值分别对库数和表数进行取余操作获取到库索引和表索引。
缺点:库和表的hash计算中存在公共因子,导致数据偏斜问题。
  • 基因法: 用原分片键中的某些基因(例如前四位)作为库的计算因子,而使用另外一些基因作为表的计算因子。
计算hash 值的片段保持充分的随机性,避免造成严重数据偏斜问题。
  • 关系表冗余:把分片键和库表索引建立一张索引表(“路由关系表”),为每个库表指定一个权限,通过权重的比例来调整数据的写入,从而实现库表数据偏斜率调整。
索引表可以通过存储在 redis 来优化性能。

每次查询操作,先去路由表中查询到数据所在的库表索引,然后再到库表中查询详细数据。

缺点:缺点是每次查询操作,需要先读取一次路由关系表,所以请求耗时可能会有一定增加。需要引入分布式事务保证数据一致性,极端情况可能带来数据的不一致。

  • 分段索引关系表: 按照号段式建立区间索引,将分片键的区间对应库的关系通过关系表记录下来,每次查询操作,先去路由表中查询到数据所在的库表索引,然后再到库表中查询详细数据。
  • 一致性 Hash 法
目录
相关文章
|
2月前
|
前端开发 JavaScript Java
技术分享:使用Spring Boot3.3与MyBatis-Plus联合实现多层次树结构的异步加载策略
在现代Web开发中,处理多层次树形结构数据是一项常见且重要的任务。这些结构广泛应用于分类管理、组织结构、权限管理等场景。为了提升用户体验和系统性能,采用异步加载策略来动态加载树形结构的各个层级变得尤为重要。本文将详细介绍如何使用Spring Boot3.3与MyBatis-Plus联合实现这一功能。
115 2
|
4月前
|
算法 Java 数据库连接
mybatis plus 主键策略
mybatis plus 主键策略
54 2
|
4月前
|
Oracle 关系型数据库 Java
mybatis使用statement.getGenreatedKeys(); useGeneratedKeys=”true”;使用自增主键获取主键值策略和Oracle不支持自增,Oracle使用序列
mybatis使用statement.getGenreatedKeys(); useGeneratedKeys=”true”;使用自增主键获取主键值策略和Oracle不支持自增,Oracle使用序列
|
6月前
|
算法 BI 数据库
MyBatisPlus查询条件设置、映射匹配兼容性、id生成策略、多数据操作
MyBatisPlus查询条件设置、映射匹配兼容性、id生成策略、多数据操作
374 3
|
6月前
|
SQL 存储 算法
Mybatis-Plus- CRUD接口-主键策略-自动填充和乐观锁-分页-逻辑删除-条件构造器和常用接口
Mybatis-Plus- CRUD接口-主键策略-自动填充和乐观锁-分页-逻辑删除-条件构造器和常用接口
|
6月前
|
缓存 Java 数据库连接
MyBatis三级缓存实战:高级缓存策略的实现与应用
MyBatis三级缓存实战:高级缓存策略的实现与应用
137 0
MyBatis三级缓存实战:高级缓存策略的实现与应用
|
6月前
|
Apache
修改mybatis-plus更新策略
修改mybatis-plus更新策略
128 0
|
6月前
|
算法 Scala 数据库
MyBatisPlus-ASSIGN_ID、ASSIGN_UUID策略、雪花算法及简化配置
MyBatisPlus-ASSIGN_ID、ASSIGN_UUID策略、雪花算法及简化配置
1246 0
|
6月前
|
关系型数据库 MySQL 数据库
MyBatisPlus-AUTO策略及INPUT策略
MyBatisPlus-AUTO策略及INPUT策略
135 0
|
SQL 算法 关系型数据库
Mybatis-Plus3.0默认主键策略导致自动生成19位长度主键id的坑
Mybatis-Plus3.0默认主键策略导致自动生成19位长度主键id的坑
106 0