1. 程式人生 > 實用技巧 >動態切換資料來源專案例項

動態切換資料來源專案例項

專案背景介紹

由於資料量較大,一共有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() {
    //切換資料來源
  }
}

這樣,相當於就是在另外的執行緒中進行處理,可以正常切換。