路由組件構建方案(分庫分表)V1
- 2022 年 10 月 20 日
- 筆記
- springboot, 設計模式, 項目實現
路由組件構建方案V1
實現效果:通過註解實現數據分散到不同庫不同表的操作。
實現主要以下幾部分:
- 數據源的配置和加載
- 數據源的動態切換
- 切點設置以及數據攔截
- 數據的插入
涉及的知識點:
- 分庫分表相關概念
- 散列算法
- 數據源的切換
- AOP切面
- Mybatis攔截器
數據源的配置和加載
獲取多個數據源我們肯定需要在yaml
或者properties
中進行配置。所以首先需要獲取到配置信息;
定義配置文件中的庫和表:
server:
port: 8080
# 多數據源路由配置
router:
jdbc:
datasource:
dbCount: 2
tbCount: 4
default: db00
routerKey: uId
list: db01,db02
db00:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxx:3306/xxxx?useUnicode=true
username: xxxx
password: 111111
db01:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxx:3306/xxxxx?useUnicode=true
username: xxxxx
password: 111111
db02:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxx:3306/xxxx?useUnicode=true
username: xxxxx
password: 111111
mybatis:
mapper-locations: classpath:/com/xbhog/mapper/*.xml
config-location: classpath:/config/mybatis-config.xml
為了實現並且使用自定義的數據源配置信息,啟動開始的時候讓SpringBoot定位位置。
首先類加載順序:指定自動配置;
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xbhog.db.router.config.DataSourceAutoConfig
針對讀取這種自定義較大的信息配置,就需要使用到 org.springframework.context.EnvironmentAware
接口,來獲取配置文件並提取需要的配置信息。
public class DataSourceAutoConfig implements EnvironmentAware {
@Override
public void setEnvironment(Environment environment){
......
}
}
屬性配置中的前綴需要跟路由組件中的屬性配置:
這裡設置成什麼,在配置文件中就要設置成對應名字
String prefix = "router.jdbc.datasource.";
根據其前綴獲取對應的庫數量dbCount
、表數量tbCount
以及數據源信息dataSource
;
//庫的數量
dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
//表的數量
tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
//分庫分表數據源
String dataSources = environment.getProperty(prefix + "list");
針對多數據源的存在,使用Map
進行存儲:Map<String,Map<String,Object>> daraSources
;
for(String dbInfo : dataSources.split(",")){
Map<String,Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
dataSourceMap.put(dbInfo,dataSourceProps);
}
通過dataSource
方法實現數據源的實例化:把基於從配置信息中讀取到的數據源信息,進行實例化創建。
將獲得的信息放到DynamicDataSource
類(父類:DataSource
)中進行實例化(setTargetDataSources
,setDefaultTargetDataSource
);
將我們自定義的數據源加入到Spring
容器管理中。
//創建數據源
Map<Object, Object> targetDataSource = new HashMap<>();
//遍曆數據源的key和value
for(String dbInfo : dataSourceMap.keySet()){
Map<String, Object> objectMap = dataSourceMap.get(dbInfo);
targetDataSource.put(dbInfo,new DriverManagerDataSource(objectMap.get("url").toString(),
objectMap.get("username").toString(),objectMap.get("password").toString()));
}
//這是數據源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSource);
//defaultDataSourceConfig的輸入點
dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(defaultDataSourceConfig.get("url").toString(),
defaultDataSourceConfig.get("username").toString(),defaultDataSourceConfig.get("password").toString()));
return dynamicDataSource;
到這裡前置的配置都在spring中完成,後續是對數據的插入,也就是mybatis
的操作:包含庫表的隨機計算和數據攔截器的實現。
動態切換數據源
路由切換的實現通過AbstractRoutingDataSource
抽象類,該類充當了DataSource
的路由中介, 在運行的時候, 根據某種key值來動態切換到真正的DataSource
上。繼承了AbstractDataSource
且AbstractDataSource
實現了DataSource
;
在AbstractRoutingDataSource
根據方法determineTargetDataSource
:
檢索當前目標數據源。確定當前查找鍵,在
targetDataSources
映射中執行查找,必要時退回到指定的默認目標數據源。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
裏面使用determineCurrentLookupKey
方法來確定當前查找的鍵(數據源key);
抽象方法
determineCurrentLookupKey()
返回DataSource
的key值,然後根據這個key從resolvedDataSources
這個map里取出對應的DataSource
,如果找不到,則用默認的resolvedDefaultDataSource
。
/**
*確定當前查找鍵。這通常用於檢查線程綁定的事務上下文。
*允許任意鍵。返回的鍵需要匹配由resolveSpecifiedLookupKey方法解析的存儲查找鍵類型
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
所以我們只需要重寫determineCurrentLookupKey
,指定我們切換數據源的名字即可;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return "db"+ DBContextHolder.getDBKey();
}
}
在這部分對應上了前面創建數據源的操作,實現的該DynamicDataSource
,並傳入了默認數據源(setDefaultTargetDataSource
)和目標數據源(setTargetDataSources
);
自定義切點
前期數據源的配置和信息已經放到Spring
容器中,可隨時使用;根據註解通過攔截器攔截方法中的數據。進行分庫分表的操作,通過擾動函數進行計算,將結果保存到ThreadLocal
中,方便後續讀取。
註解實現:
分庫註解:首先設置三要素。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DBRouter {
/** 分庫分表字段 */
String key() default "";
}
通過自定義切點@Around(**"aopPoint()&&@annotation(dbRouter)"**)
,實現使用註解的時候就攔截對應的值:
在環繞處理的時候,判斷方法上註解是否對應有值,有的話通過註解傳入的value
和方法傳入的參數進行路由計算:
計算規則:
- 獲取方法傳入的參數
- 計算庫表總數量:
dbCount*tbCount
- 計算idx:
**int **idx = (size -1) & (Key.hashCode() ^ (Key.hashCode() >>> 16))
- 簡單說明:與運算標識符後面,通過混合高位和低位,增大隨機性
**int **dbIdx = idx / dbCount() + 1
**int **tbIdx = idx - tbCount() * (dbIdx - 1)
通過上述操作,將計算的記過保存到ThreadLocal
中。
獲取方法傳入的參數:
private String getAttrValue(String dbKey, Object[] args) {
if(1 == args.length){
return args[0].toString();
}
String filedValue = null;
for(Object arg : args){
try{
if(StringUtils.isNotBlank(filedValue)){
break;
}
filedValue = BeanUtils.getProperty(arg,dbKey);
}catch (Exception e){
log.info("獲取路由屬性失敗 attr:{}", dbKey,e);
}
}
return filedValue;
}
自定義攔截器
我們定義了Interceptor將攔截StatementHandler
(在SQL
語法構建處理攔截)中參數類型為Connection的prepare方法,具體需要深入mybatis
源碼;
主要功能:在執行SQL語句前攔截,針對相關功能實現SQL的修改
在上述文章中主要是針對分庫分表前做準備,下面才是決定數據入哪個庫哪張表
通過StatementHandler
(MyBatis直接在數據庫執行SQL腳本的對象)獲取mappedStatement
(MappedStatement維護了一條<select|update|delete|insert>節點的封裝),根據maperdStatement
獲取自定義註解dbRouterStrategy
,判斷是否進行分表操作;
Class<?> clazz = Class.forName(className);
DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){
return invocation.proceed();
}
dbRouterStrategy
註解默認是false
不分表,直接進行數據的插入【更新】;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DBRouterStrategy {
boolean splitTable() default false;
}
如果分表註解存在或者分表參數是true
,則進行以下四步:
-
獲取SQL
BoundSql
:表示動態生成的SQL
語句以及相應的參數信息。
//獲取SQL
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
- 匹配SQL
通過正則匹配分割【insert/select/update】和表名,方便後續表名的拼接。
//替換SQL表名USER為USER_3;
Matcher matcher = pattern.matcher(sql);
String tableName = null;
if(matcher.find()){
tableName = matcher.group().trim();
}
- 拼接SQL
則通過反射修改SQL
語句,並且替換表名;其中filed.set()
將指定對象實參上由此field對象表示的字段設置為指定的新值。如果基礎字段具有基元類型,則自動解開新值
assert null != tableName;
String replaceSQL = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());
//通過反射修改SQL語句
Field filed = boundSql.getClass().getDeclaredField("sql");
filed.setAccessible(true);
filed.set(boundSql,replaceSQL);
參考文章
//www.cnblogs.com/aheizi/p/7071181.html
//blog.csdn.net/wb1046329430/article/details/111501755
//blog.csdn.net/supercmd/article/details/100042302
//juejin.cn/post/6966241551810822151