??最近業務中出現了多商戶多租戶的邏輯,所以需要分庫,項目框架使用了mybatisplus所以我們自然而然的選擇了同是baomidou開發的dynamic.datasource來實現多數據源的切換。在使用初期程序運行都很好,但之后發現在調用com.baomidou.mybatisplus.extension.service.IService.saveBatch時@DS切換數據源會失效。
問題原因
??進入saveBatch方法我們可以看到方法上添加了Transactional,我們知道Transactional用來管理事務,在事務開啟后進行數據庫的切換時并不會生效,源代碼如下,當線程持有數據庫連接時會復用當前線程綁定的數據庫連接,否則綁定默認的主庫連接,既然最終連接到主庫,說明@DS并沒有生效。
嘗試解決問題step1
??前往Github的dynamic-datasource代碼倉庫查看Issues,發現了大量的關于@DS多數據源切換無效的Issues,but官方看起來很傲慢,要么直接回復未復現,要么直接關閉。
??只有一條信息比較有用,在調用被Transactional注解的方法的方法或類上添加@DS注解,我試了有效果。
??但是我認為在Service和方法上加@DS注解并不合適,Spring框架就是因為清晰明了的分層結構深受大家喜愛,控制層專注Web,Service層專注業務邏輯,持久層專注數據庫交互,所以@DS數據庫切換放在Mapper我覺得是合理的,而不應該為了解決問題硬生生的放在方法和類上來破壞這種分層結構。況且mybatisplus中那么多添加了Transactional的方法在調用的地方我都需要重寫并添加@DS這太2了。
嘗試解決問題step2
??離開Github我馬上找google和度娘,畢竟我遇到的這點問題前輩們可能早就遇到了并給出了解決方案。
這里不得不吐槽一下中文技術博客的現狀,很多偷文賊將別人的文章偷走,也不標轉載自哪里,導致大量博客內容雷同且存在很多詞不達意的內容。因為喜歡所以才會分享表達,不喜歡不熱愛你說你偷別人文章干啥。
??根據搜索引擎的結果,主要分為3種解決方案。
- 在Service類或者方法上添加@DS注解
- 在調用帶有Transactional注解的方法前切換數據庫
- 自己實現TransactionManager在使用Transactional時手動指定來替換Spring默認的DataSourceTransactionManager
??方案1在step1我自己并不認可
??方案2相對方案1更加靈活,畢竟因為在方法中切換,可以根據不同的Service來獲取需要切換的數據源,但這種方案個人覺得侵入性太強,需要對使用了mybatisplus批量方法的Service全部進行處理
??方案3我認為風險太高,自己實現TransactionManager事務、異步、同步等都需要考慮到還要保證單元測試盡可能的覆蓋,我不認為短時間內能做的比迭代了多年的框架更好,所以也放棄
嘗試解決問題step3
??我們知道Spring因為AOP特性可以輕松的實現在不對原有代碼侵入的情況下對特定內容進行增強,所以我決定使用切面編程對mybatisplus中帶有Transactional注解的方法進行攔截,然后手動切換數據庫,注冊切面部分很快完成,剩下的就是調試數據庫切換。
??數據庫切換我使用了dynamic.datasource包內的DynamicDataSourceContextHolder.push方法,但遺憾的是數據庫切換一直不成功并卡了很久,期間使用DynamicRoutingDataSource.setPrimary方法將需要使用的數據庫指定為主庫運行成功,但這種騷操作肯定不合適,將別的庫指定為主庫風險肯定很大。
??之后就是漫長的Debug,不斷的F7、F8,一直沒有頭緒,我在方法上添加了@DS注解,并關閉了我的切面類再進行調試,突然發現了點不一樣的東西,不知道有沒有敏感的同學發現端倪。
??請關注chain變量,里面包含3個攔截器,更重要的是動態數據庫切換的攔截器在事務攔截器前面,而我們的目的不就是在事務開啟前切換數據庫嗎,那我現在的問題就是我的切面類在事務后執行的,所以調整我的切面類執行優先級就好了,立馬把Order注解抬上來,執行程序完美運行。
切面類最終代碼
??如果你也遇到了調用mybatisplus中批量方法無法切換多數據源的話,可直接拷貝安全食用,不會對現有的人和代碼侵入和更改。如果你只是處理Transactional和@DS的沖突,你可以對切面類的作用范圍小小修改即可解決你的問題。
package com.spman.common.aspect;import com.alibaba.fastjson2.JSON;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.lang.reflect.Field;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Slf4j
@Aspect
@Order(0)
@Component
public class MyBatisPlusServiceTransactionalAspect {/*** 存儲當前切面主動切換的數據庫, 在方法執行完成后主動出棧*/private static final ThreadLocal<String> DS_KEY = new ThreadLocal<>();@Pointcut("execution(* com.baomidou.mybatisplus.extension.service.IService+.*(..))")public void myBatisPlusMethodPointcut() {}@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")public void transactionalPointcut() {}@Before("myBatisPlusMethodPointcut() && transactionalPointcut()")public void beforeHandler(JoinPoint joinPoint) {String argsJson = JSON.toJSONString(joinPoint.getArgs());ServiceImpl<?, ?> target = (ServiceImpl<?, ?>)joinPoint.getTarget();String methodName = target.getClass().getTypeName() + "." + joinPoint.getSignature().getName();log.info("MyBatisPlusServiceAspect攔截到{}開始執行, 參數列表->{}", methodName, argsJson);Class<? extends BaseMapper<?>> mapperClass = getMapperClass(target);DS dsAnnotation = getDSAnnotation(mapperClass);if (dsAnnotation == null) {log.info("{}未綁定DS注解, 跳過數據源切換", mapperClass.getName());} else {DS_KEY.set(dsAnnotation.value());DynamicDataSourceContextHolder.push(dsAnnotation.value());log.info("{}已綁定DS注解, 已主動切換數據源為{}", mapperClass.getName(), dsAnnotation.value());}}@After("myBatisPlusMethodPointcut() && transactionalPointcut()")public void afterHandler(JoinPoint joinPoint) {String dsKey = DS_KEY.get();ServiceImpl<?, ?> target = (ServiceImpl<?, ?>)joinPoint.getTarget();String methodName = target.getClass().getTypeName() + "." + joinPoint.getSignature().getName();if (dsKey != null && !dsKey.isEmpty()) {DynamicDataSourceContextHolder.poll();log.info("DS_KEY線程變量為{}, 已執行數據源變量出棧操作", dsKey);} else {log.info("DS_KEY線程變量不存在, 跳過數據源變量出棧操作");}log.info("MyBatisPlusServiceAspect攔截到{}結束執行", methodName);}/*** 從ServiceImpl中獲取service綁定的mapper** @param target ServiceImpl實例*/@SneakyThrowsprivate Class<? extends BaseMapper<?>> getMapperClass(ServiceImpl<?, ?> target) {Field mapperClassField = target.getClass().getSuperclass().getDeclaredField("mapperClass");mapperClassField.setAccessible(true);return (Class<? extends BaseMapper<?>>) mapperClassField.get(target);}/*** 根據BaseMapper接口獲取標記的DS注解** @param clazz 繼承自BaseMapper的mapper接口*/public static DS getDSAnnotation(Class<? extends BaseMapper<?>> clazz) {if (clazz == null) return null;DS target = clazz.getAnnotation(DS.class);// 找不到DS注解時從繼承的接口上繼續查找if (target == null) {for (Class<?> parentInterface: clazz.getInterfaces()) {target = getDSAnnotation((Class<? extends BaseMapper<?>>)parentInterface);if (target != null) return target;}}return target;}
}
如果真的需要解決問題還是需要自己耐心的跟進,拒絕為了解決問題而解決問題。
參考資料
[1] mybatisplus官網: https://baomidou.com/
[2] dynamic-datasource代碼倉庫: https://github.com/baomidou/dynamic-datasource
[3] Spring之AOP的詳細講解: https://blog.csdn.net/m0_74097410/article/details/137476783