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 法
目录
相关文章
|
5月前
|
存储 大数据 数据库
分库分表知识总结(三)之水平分表
分库分表知识总结(三)之水平分表
47 0
|
12月前
|
存储 数据处理 数据库
分表方案有哪些
分表方案有哪些
82 0
|
12月前
|
存储 程序员 数据库
如何选择合适的分表分库方案
如何选择合适的分表分库方案
68 0
|
12月前
|
存储 数据库连接 数据库
分库方案有哪些
分库方案有哪些
75 0
|
12月前
|
存储 JavaScript Java
亿级别大表拆分 —— 记一次分表工作的心路历程
亿级别大表拆分 —— 记一次分表工作的心路历程
|
SQL 存储 算法
SpringBoot整合ShardingSphere实现分表分库&读写分离&读写分离+数据库分表
SpringBoot整合ShardingSphere实现分表分库&读写分离&读写分离+数据库分表
1288 0
SpringBoot整合ShardingSphere实现分表分库&读写分离&读写分离+数据库分表
|
算法
Mycat分表分库规则--待发文
Mycat分表分库规则--待发文
63 0
Mycat分表分库规则--待发文
|
存储 SQL 运维
2、【ShardingSphere】做优化上来就分库分表?请慎重分库分表
读写分离,基本是目前商业开发最可靠的手段了。让我们有了更好的数据查询效率。最大的缺陷在于读写分离会增加MySQL服务器的预算。同时MySQL在高并发的情况下,slave也会有延迟,错误等。
222 0
|
存储 SQL 关系型数据库
使用MyCat单库分表实战详解
本文目录 1. 场景 2. 实现 3. 配置真实服务器信息 4. 配置路由规则 5. 配置MyCat服务信息 6. 启动测试
851 0
使用MyCat单库分表实战详解
|
存储 数据库 索引
数据分表分库的基本思路
当一个数据库被创建之后,随着时间的推移和业务量的增加,数据库中的表以及表中的数据量都会越来越多,就有可能会出现两种弊端: (1)数据库的存储资源是有限的,其负载能力也是有限的,数据的大量积累肯定会导致其处理数据的能力下降; (2)数据量越多,那么对数据的增删改查等操作的开销也会越来越大; 所以,当出现如上两种情况,分库分表势在必行。
86 0