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