為什么要分表
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
以上內容屬個人學習總結,如有不當之處,歡迎在評論中指正