動態切換資料來源專案例項
專案背景介紹
由於資料量較大,一共有13個結構一致的分庫,每個分庫中檔案記錄表拆分為100張表,命名為entity_00-99,表的主鍵為stbh,40位長度,前4位為分割槽碼,依次從1001-1013。stbh舉例,比如下面這個stbh
1001208208f066b9b34f49b32d485fc58c6c8519
意味著,儲存的庫為1001分割槽所在的庫,存入的檔案表為entity_19表。
再比如,stbh=100320f727cbac2add46a5a85e8f31d5aa32b191,表示資料儲存在1003分割槽對應的庫,存入的檔案表為該庫上的entity_91表。
整個系統有一張sys_db表,表裡配置了1001-1013的資料庫配置資訊,如下
1001 url driver username password
1002 url driver username password
......
資料在操作時,總是能拿到分割槽資訊,這樣以便於能根據分割槽資訊找到要操作的庫,無論是新增、修改、刪除或是查詢。
不考慮動態切換資料來源的情形,可以這樣考慮,即專案啟動的時候,載入sys_db表,構造一個fqMap,key=分割槽碼,value=資料庫資源資訊或者根據資源資訊得到的dataSource甚至是連線,這樣,任何時候操作的時候,總是能根據分割槽碼得到要操作的資料庫資訊進行業務處理。
當然,這是一種實現方式,不是本文要討論的內容。本文討論的主要是使用mybatis-plus工具用於持久化操作下的動態資料來源切換。
本文主要討論的是spring的動態資料來源切換。
要實現spring的動態資料來源管理和切換,我們需要以下類和程式碼(注意,這裡僅給出核心程式碼,並不是完善的可執行的程式碼)。
ShardingDataSourceConfig 專案啟動時會載入
@Configuration
@Slf4j
public class ShardingDataSourceConfig {
rs = statement.executeQuery("select * from sys_db where app_id = 1");
while (rs.next()) {
String urlJdbc = rs.getString("url");String userNameJdbc = rs.getString("user_name");
String passwordJdbc = rs.getString("password");
String fydm = rs.getString("fydm");
String driverJdbc = rs.getString("driver");
String area_id = rs.getString("area_id");
Map<String, String> map = new HashMap<>();
map.put("url", urlJdbc);
map.put("driverClassName", driverJdbc);
map.put("username", userNameJdbc);
map.put("password", passwordJdbc);
map.put("maxActive",maxActive);
map.put("minIdle",minIdle);
map.put("initialSize",initialSize);
targetDataSources.put(area_id, DruidDataSourceFactory.createDataSource(map));
}
}
MultipleDataSourceChoose 繼承 AbstractRoutingDataSource
public class MultipleDataSourceChoose extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return HandlerDataSource.getDataSource();
}
}
HandlerDataSource 用執行緒控制當前選擇的庫
public class HandlerDataSource {
private HandlerDataSource (){
throw new IllegalStateException("HandlerDataSource class");
}
private static ThreadLocal<String> handlerThredLocal = new ThreadLocal<>();
public static void putDataSource(String datasource) {
handlerThredLocal.set(datasource);
}
public static String getDataSource() {
return handlerThredLocal.get();
}
public static void clear() {
handlerThredLocal.remove();
}
}
HandlerDataSourceAop 切面aop
@Aspect
@Slf4j
@Component
@Order(1)
public class HandlerDataSourceAop {
//切面到service是為了方便從service層的方法上拿到引數判斷選擇的庫
@Pointcut("execution(public * com.xxx.service..*.*(..))")
public void pointcut() {
//切面
}
//根據引數來切換資料來源
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) {
Object[] objs = joinPoint.getArgs();
HandlerDataSource.putDataSource(area_id);
}
@After("pointcut()")
public void after(JoinPoint point) {
HandlerDataSource.clear();
}
}
有了以上幾個類,我們就可以開始正式操作了。
我們已經有了 EntityMapper類,也有EntityService類。
@Service
public class EntityService
@Autowired
private EntityMapper entityMapper;
public void add(){}
public void query(){}
public void delete(){}
public void update(){}
由於前面定義了切面,進入方法時,會自動根據引數進行資料來源切換操作,由於業務的特點,add、delete、update均會基於同一個庫進行操作,無需額外處理。
這裡,重點要討論的就是查詢,查詢由於業務特殊需要,可能會傳入一批stbh,這一批的stbh可能是分散在各個分庫的各個不同的分表的,這樣,進入到query方法時,無論此時動態資料來源切換到哪個庫,都是不夠的,在業務實現時,需要進一步根據stbh切換資料來源。
我們先將所有的stbh,按分割槽碼分組,在相同分割槽的stbh分為一組,得到一個stbhMap,如下
1001 stbh1,stbh2,stbh3
1002 stbh4,stbh5,stbh6
1012 stbh7,stbh8,stbh9
假如此時我們有前面講的fqMap,可以直接根據分割槽碼得到對應庫的連結,進行查詢即可。
要實現資料來源切換,基於前面準備的類,我們只需要在entityMapper進行selectById前進行切換操作即可,如下
遍歷1001對應的一批stbh時
HandlerDataSource.putDataSource("1001");
for(String stbh : stbhList){
entityMapper.seleteById(stbh);
}
遍歷1002分割槽對應的一批stbhHandlerDataSource.putDataSource("1002");
for(String stbh : stbhList){
entityMapper.seleteById(stbh);
}
這樣一看,很easy嘛,結果卻是,理想很豐滿,現實很骨感。資料來源切換失敗,除錯發現,資料來源始終是進入query方法前aop切面指定的那個資料來源,裡面的切換動作沒有生效。
那麼,問題出在哪裡呢?
原因:經分析,原因出在資料來源切換是通過執行緒控制的,上面這些程式碼和query方法均在同一個執行緒下,無法成功切換。
為了成功切換,query方法中啟動額外的執行緒,如下
Thread t = new Thread(new QueryWorker());
t.start();
上述切換資料來源的操作放在QueryWorker類中
public classQueryWorker implements Runnable{
public void run() {
//切換資料來源
}
}
這樣,相當於就是在另外的執行緒中進行處理,可以正常切換。