MyBatis+Mysql分庫分表的案例分析
**多資料來源動態切換 *分庫分表***
參考 [AbstractRoutingDataSource動態資料來源切換,AOP實現動態資料來源切換](https://blog.csdn.net/u012881904/article/details/77449710)
案例原理:
主要是DynamicDataSource繼承AbstractRoutingDataSource重寫determineCurrentLookupKey進行了lookupkey的set,
利用AOP在業務裡實現spring-jdbc的AbstractRoutingDataSource。
/** * 資料來源動態切換類 */ public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DbContextHolder.getDbKey(); //獲取當前資料來源 } }
dataSource每次getConnection之前都要通過lookupkey獲取指定的DataSource(這裡的get(lookupkey)由別名獲取到資料來源,是因為在application-db.xml裡註冊了資料來源bean)
/** * 以下為org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource.class原始碼 * * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs * a lookup in the {@link #setTargetDataSources targetDataSources} map, * falls back to the specified * {@link #setDefaultTargetDataSource default target DataSource} if necessary. * @see #determineCurrentLookupKey() */ 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; }
資料來源配置:
<!-- 配置資料來源 開始--> <bean id="baseDataSource" class="com.alibaba.druid.pool.DruidDataSource" abstract="true" init-method="init" destroy-method="close"> <!-- ...... --> </bean> <!-- 配置資料來源 結束--> <!-- 主db開始 --> <bean id="datasource" parent="baseDataSource"> <property name="url"> <value>${mysql.jdbc.url}</value> </property> <property name="username"> <value>${common.mysql.jdbc.user}</value> </property> <property name="password"> <value>${common.mysql.jdbc.password}</value> </property> </bean> <!-- 主db結束 --> <!-- 主 DB-1 開始--> <bean id="masterDatasource1" parent="baseDataSource"> </bean> <!-- 主DB-1 結束--> <!-- ..... --> <!-- 主DB-6 結束--> <!-- 從 DB-1 開始--> <bean id="slaveDatasource1" parent="baseDataSource"> </bean> <!-- 從DB-1 結束--> <!-- ..... --> <!-- 從DB-6 結束--> <!-- 切換資料來源(繼承了AbstractRoutingDataSource) 開始 --> <bean id="dataSource" class="com.xxx.datasource.db.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="msd1" value-ref="masterDatasource1" /> <!-- ..... --> <entry key="msd6" value-ref="masterDatasource6" /> <entry key="sld1" value-ref="slaveDatasource1" /> <!-- ..... --> <entry key="sld6" value-ref="slaveDatasource6" /> </map> </property> <property name="defaultTargetDataSource" ref="datasource" /> </bean> <!--切換資料來源 結束 --> <!-- 配置資料來源路由 --> <bean id="dbRuleSet" class="com.xxx.datasource.bean.RouterSet"> <property name="dbNumber" value="6"></property><!-- db個數 --> <property name="tableNumber" value="6"></property><!-- 每個庫裡分表個數 --> <property name="masterDbKeyArray"> <list> <value>msd1</value> <!-- ..... --> <value>msd6</value> </list> </property> <property name="slaveDbKeyArray"> <list> <value>sld1</value> <!-- ..... --> <value>sld6</value> </list> </property> <property name="shardingTableArray"> <list> <value>xxxxx</value> <value>xxxxx_express</value> </list> </property> </bean>
AOP通過使用者id計算key值匹配配置檔案裡定義好的Datasource集合 得到具體資料來源。 從而繼續執行MyBatis後續分表和Sql操作。
在service層利用@Router的切面織入業務: 計算分庫分表key,去setLookupkey的程式碼,每次檢查到@Router就去切換資料來源,執行資料操作。
service層加@Router註解
/**
*
* @param userId
* @param orderIds
* @return
*/
@Router
public Map<String, Object> getOrder(String userId, String[] orderIds) {
//do something
return null;
}
切面前置增強:
/**
* 切換到分庫
*
* @param jp
* @return
* @throws NoSuchMethodException
* @throws RouterException
* @throws Throwable
*/
@Before("aopPoint()")
public Object doRoute(JoinPoint jp) throws NoSuchMethodException, RouterException {
Method method = getMethod(jp);
Router router = method.getAnnotation(Router.class);
String routeField = router.routerField();
Object[] args = jp.getArgs();
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] argsName = methodSignature.getParameterNames();
//根據快取的userId和請求提供的進行比較,計算routeFieldValue。“計算邏輯”見下文
String routeFieldValue = getRouteFieldValue(args, argsName, routeField);
if (StringUtils.isNotEmpty(routeFieldValue)) {
dBRouter.doRoute(routeFieldValue, router);
} else {
log.error("分庫分表字段為空,未切換資料來源");
}
return true;
}
分庫分表原理:
spring檢查到aop元件進入切面處理程式碼DBRouterInterceptor
前置增強進行資料來源切換:
根據當前使用者id進行 doRoute,若引數有userId,以入參為準,
計算邏輯:
a.id轉換為utf-8下Base64位的string,獲取此string的雜湊值的絕對值
b.再將此雜湊值除以10000去餘數得到整數值
c.配合資料來源配置的路由陣列例如mode=6*6, b步驟得到的整數值除以mode取餘除以路由的庫數 為計算出的dbIndex
d.b步驟得到的整數值除以路由的單庫的表數取餘為tbIndex
將dbIndex tbIndex全放入路由物件RouterInfo,格式化表路由tableIndex之後存入DbContexHolder(此處格式化是因為表名字首一致,字尾_00遞增)
再將庫路由dbIndex存入DbContexHolder
*至此*路由全部計算出存入DbContexHolder。需要理解DbContexHolder裝載的執行緒常量ThreadLocal特性
DbContexHolder的工作原理參見 “原理” 因為是繼承了dataSource的AbstractRoutingDataSource才可以進行lookupkey的set操作。
完成庫的路由計算,切換了資料來源,然後就是MyBatis的分表。
ShardingInterceptor攔截器攔截Executor實際的sql執行操作,路由到DbContexHolder裡指定的表。具體原理再看
一個多庫多表的請求從路由到指定庫、指定表即以上步驟。目前設定的是:執行完之後AOP後置增強會切回主資料來源,異常也會切回主資料來源。
小結:專案是水平分表,將一個大表分了幾個欄位一模一樣的表。資料表裡的seq實際是以 庫數字字尾_表數字字尾_數字串 拼接進行唯一標識。資料這樣分散式否均勻,還是取決於使用者id的劃分是否合理(計算邏輯是否合理)。
如果有查全量的資料,這樣分好合資料嗎?一個使用者的資料會分到指定庫指定表,如果有跨庫業務怎麼辦。目前,日統計和週期統計都是單獨task服務去做的。
垂直分表目前還沒理解。待續
持續更新
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------