mybatis-plus分表

cn2024發表於2024-07-05

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

相關文章