mybatis-plus還可以這樣分表


為什么要分表

Mysql是當前互聯網系統中使用非常廣泛的關系數據庫,具有ACID的特性。

但是mysql的單表性能會受到表中數據量的限制,主要原因是B+樹索引過大導致查詢時索引無法全部加載到內存。讀取磁盤的次數變多,而磁盤的每次讀取對性能都有很大的影響。

這時一個簡單可行的方案就是分表(當然土豪也可以堆硬件),將一張數據量龐大的表的數據,拆分到多個表中,這同時也減少了B+樹索引的大小,減少磁盤讀取次數,提高性能。

兩種基礎分表邏輯

說完了為什么要分表,下面聊聊業務開發中常見的兩種基礎的分表邏輯。

按日期分表

這種方式通常會在表名的最后加上年月日,主要適用於按日期划分的統計數據或操作記錄。在線實時展示的只有最近表中的數據,其他數據用於離線統計等。

按id取模分表

這種方式需要一個id生成器,例如snowflake id或分布式id服務。它保證了相同id的數據都在一張表中,主要適用於保存用戶基礎信息,系統中的資源信息,購買記錄等。當然這種分表方式擴展性較差,后期數據持續增多后需要按id大小分庫再分表處理。

下面看下這兩種分表邏輯在mybatis-plus中的實現。

Mybatis-plus中的分表實現

說到java的分表中間件,可能有人會想到sharding-jdbc,作為使用很廣泛的一個分表中間件,功能也比較完善,但是使用它需要引入額外的jar包和增加學習成本。

實際上mybatis-plus本身就提供了一個分表的解決方案,配置使用都很簡單,適合快速開發系統。

動態表名處理器

沒錯,mybatis-plus提供了動態表名處理器接口TableNameHandler,只需要在系統中實現該接口,並作為插件加載到mybatis-plus中就可以使用,下面來看下詳細的步驟。

3.4版本之前的動態表名接口是ITableNameHandler,需要和分頁插件配合使用。

3.4版本新增了TableNameHandler,在方法參數上取消了MetaObject。這里用最新的版本為例,使用方式差別不大。

假設我們的系統中有兩種分表方式,按日期分表和按id取模分表。通過四個步驟來看下具體的使用示例。

1.創建日期表名處理器

先來看下日期處理的表名處理器,實現TableNameHandler接口后,在dynamicTableName方法中實現動態生成表名的邏輯,方法的返回值就是查詢時要使用的表名。

/**
 * 按天分表解析
 */
public class DaysTableNameParser implements TableNameHandler {

    @Override
    public String dynamicTableName(String sql, String tableName) {
        String dateDay = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        return tableName + "_" + dateDay;
    }
}
2.創建id取模表名處理器

再來看下按id取模表名處理器的實現,這個處理器相對日期處理就要復雜一些,主要原因為需要動態傳入用於分表的id值

在之前的版本中可以在方法中通過解析MetaObject中帶有的sql查詢信息,獲取分表使用的值。但是這種方式比較復雜,對於不同的QueryMapper分析的方式不同,比較容易出錯。新版本中的方法取消了MetaObject參數,需要使用其他方式傳入。

需要注意的是,表名處理器是作為mybatis-plus的插件,在項目啟動時實例化的。這意味着,在運行過程中只有一個對象,多線程處理過程中,一個線程對參數的修改,會影響到其他線程。為了解決這個問題,可以使用ThreadLocal來定義參數。

由於現在的框架中大部分會使用線程池,例如springboot web項目中的tomcat。所以在每次使用后,需要手動清除本次數據,防止線程復用時的影響。

具體實現如下:

/**
 * 按id取模分表處理器
 */
public class IdModTableNameParser implements TableNameHandler {
    private Integer mod;

    //使用ThreadLocal防止多線程相互影響
    private static ThreadLocal<Integer> id = new ThreadLocal<Integer>();

    public static void setId(Integer idValue) {
        id.set(idValue);
    }

    IdModTableNameParser(Integer modValue) {
        mod = modValue;
    }

    @Override
    public String dynamicTableName(String sql, String tableName) {
        Integer idValue = id.get();
        if (idValue == null) {
            throw new RuntimeException("請設置id值");
        } else {
            String suffix = String.valueOf(idValue % mod);
            //這里清除ThreadLocal的值,防止線程復用出現問題
            id.set(null);
            return tableName + "_" + suffix;
        }
    }
}
3.加載表名處理器

表名處理器實際是mybatis-plus的插件,需要在初始化時創建實例並加載。因為系統中存在兩種分表類型,在初始化時可以指定每張表使用的表名處理器。具體實現如下:

@Configuration
@MapperScan(basePackages = "com.yourcom.proname.repository.mapper.mainDb*", sqlSessionFactoryRef = "mainSqlSessionFactory")
public class MainDb {
    @Bean(name = "mainDataSource")
    @ConfigurationProperties(prefix = "dbconfig.maindb")
    public DataSource druidDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "mainTransactionManager")
    public DataSourceTransactionManager masterTransactionManager(@Qualifier(value = "mainDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "mainSqlSessionFactory")
    @ConfigurationPropertiesBinding()
    public SqlSessionFactory sqlSessionFactory(@Qualifier(value = "mainDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
      	//加載插件
        factoryBean.setPlugins(mybatisPlusInterceptor());
        return factoryBean.getObject();
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        HashMap<String, TableNameHandler> map = new HashMap<String, TableNameHandler>();

        //這里為不同的表設置對應表名處理器
        map.put("user_daily_record", new DaysTableNameParser());
        map.put("user_consume_flow", new IdModTableNameParser(10));

        dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        return interceptor;
    }
}
4.在controller中使用

下面通過controller中的三個接口,展示下使用方式:

@RestController
public class TableTestController {
    @Resource
    IUserDailyRecordService userDailyRecordService;

    @Resource
    IUserConsumeFlowService userConsumeFlowService;

    @GetMapping("user/record/today")
    public CommonResVo<UserDailyRecord> getRecordToday(Integer userId) throws Exception {
        //這里在查詢時,會根據系統當前時間,自動生成當天的表名
        UserDailyRecord userDailyRecord = userDailyRecordService.getOne(new LambdaQueryWrapper<UserDailyRecord>().eq(UserDailyRecord::getUserId, userId));
        return CommonResVo.success(userDailyRecord);
    }

    @GetMapping("user/consume/flow")
    public CommonResVo<List<UserConsumeFlow>> getConsumeFlow(Integer userId) throws Exception {
        //設置用於分表的id值
        IdModTableNameParser.setId(userId);
        List<UserConsumeFlow> userConsumeFlowList = userConsumeFlowService.list(new LambdaQueryWrapper<UserConsumeFlow>().eq(UserConsumeFlow::getUserId, userId));
        return CommonResVo.success(userConsumeFlowList);
    }

    /**
     * 新增數據
     */
    @PostMapping("user/consume/flow")
    public CommonResVo<Boolean> addConsumeFlow(@RequestBody UserConsumeFlow userConsumeFlow) throws Exception {
        Integer userId = userConsumeFlow.getUserId();
        //設置用於分表的id值
        IdModTableNameParser.setId(userId);
        userConsumeFlowService.save(userConsumeFlow);
        return CommonResVo.success(true);
    }
}

這篇對mybatis-plus動態表名處理器的介紹,通過實現TableNameHandler接口,可以按實際情況靈活定義表名的生成規則,希望對大家有幫助。

項目完整示例地址:https://gitee.com/dothetrick/web-demo/tree/tabel-shading

以上內容屬個人學習總結,如有不當之處,歡迎在評論中指正


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM