@Transactional跟@DS動(dòng)態(tài)數(shù)據(jù)源注解沖突的解決
@Transactional跟@DS動(dòng)態(tài)數(shù)據(jù)源注解沖突
背景
前陣子寫一個(gè)項(xiàng)目時(shí),有個(gè)需求是要往3個(gè)庫(kù),3個(gè)表里插入數(shù)據(jù),在同一個(gè)方法里,公司是用baomidou的@DS注解來實(shí)現(xiàn)配置動(dòng)態(tài)數(shù)據(jù)源的。這是背景,然后呢,我在一個(gè)service方法里,就操作了這三張表,同時(shí),我還加上了@Transactional注解,因?yàn)樵摲椒ㄊ莻€(gè)save方法,我就加上了事務(wù)。
偽代碼如下:
spring: datasource: dynamic: primary: A datasource: A: url:.. driver-class-name: com.mysql.jdbc.Driver username: root password: B: url: .. driver-class-name: com.mysql.jdbc.Driver username: root password: C: url: .. driver-class-name: com.mysql.jdbc.Driver username: root password:
@Mapper @DS("A") public interface AMapper{ @Insert("insert into a ...") void save(); }
@Mapper @DS("B") public interface BMapper{ @Insert("insert into b ...") void save(); }
@Mapper @DS("C") public interface CMapper{ @Insert("insert into c ...") void save(); }
public class aService{ @Autowired private aMapper、bMapper、cMapper @Transactional public void save(){ aMapper.save(); bMapper.save(); #報(bào)錯(cuò) cMapper.save(); } }
在開發(fā)完成用postman自測(cè)時(shí),發(fā)現(xiàn)bMapper.save()那一行報(bào)錯(cuò)了,報(bào)錯(cuò)內(nèi)容:找不到b表。如果把@Transactional注釋掉,代碼正常運(yùn)行,數(shù)據(jù)成功落庫(kù)。我們明明在3個(gè)mapper上面都加了@DS注解來切換數(shù)據(jù)源,那為啥加了@Transactional就不行了呢?
@Transactional執(zhí)行流程
- save方法添加了 @Transactional 注解,Spring 事務(wù)就會(huì)生效。此時(shí),Spring TransactionInterceptor 會(huì)通過 AOP 攔截該方法,創(chuàng)建事務(wù)。而創(chuàng)建事務(wù),勢(shì)必就會(huì)獲得數(shù)據(jù)源。那么,TransactionInterceptor 會(huì)使用 Spring DataSourceTransactionManager 創(chuàng)建事務(wù),并將事務(wù)信息通過 ThreadLocal 綁定在當(dāng)前線程。
- 而事務(wù)信息,就包括事務(wù)對(duì)應(yīng)的 Connection 連接。那也就意味著,還沒走到 OrderMapper 的查詢操作,Connection 就已經(jīng)被創(chuàng)建出來了。并且,因?yàn)槭聞?wù)信息會(huì)和當(dāng)前線程綁定在一起,在 OrderMapper 在查詢操作需要獲得 Connection 時(shí),就直接拿到當(dāng)前線程綁定的 Connection ,而不是 OrderMapper 添加 @DS 注解所對(duì)應(yīng)的 DataSource 所對(duì)應(yīng)的 Connection 。
- OK ,那么我們現(xiàn)在可以把問題聚焦到 DataSourceTransactionManager 是怎么獲取 DataSource 從而獲得 Connection 的了。對(duì)于每個(gè) DataSourceTransactionManager 數(shù)據(jù)庫(kù)事務(wù)管理器,創(chuàng)建時(shí)都會(huì)傳入其需要管理的 DataSource 數(shù)據(jù)源。在使用 dynamic-datasource-spring-boot-starter 時(shí),它創(chuàng)建了一個(gè) DynamicRoutingDataSource ,傳入到 DataSourceTransactionManager 中。
- 而 DynamicRoutingDataSource 負(fù)責(zé)管理我們配置的多個(gè)數(shù)據(jù)源。例如說,本示例中就管理了 a、b、c 三個(gè)數(shù)據(jù)源,并且默認(rèn)使用 a 數(shù)據(jù)源。那么在當(dāng)前場(chǎng)景下,DynamicRoutingDataSource 需要基于 @DS 獲得數(shù)據(jù)源名,從而獲得對(duì)應(yīng)的 DataSource ,結(jié)果因?yàn)槲覀冊(cè)?Service 方法上,并沒有添加 @DS 注解,所以它只好返回默認(rèn)數(shù)據(jù)源,也就是 a 。故此,就發(fā)生了 找不到表 的異常。
我們?cè)谏厦媪私獾剑驗(yàn)锧Transactional會(huì)創(chuàng)建事務(wù)然后獲得數(shù)據(jù)源,因?yàn)槲覀僺ervice方法上沒有@DS注解,就拿了默認(rèn)數(shù)據(jù)源,并且在這之后,這個(gè)事務(wù)信息會(huì)通過threadLocal跟當(dāng)前線程綁定,事務(wù)信息包括了connection連接,也就意味著,在進(jìn)入這個(gè)service方法的時(shí)候,當(dāng)前事務(wù)就綁定了數(shù)據(jù)源a,在運(yùn)行到bMapper.save()時(shí),因?yàn)閏onnection已經(jīng)存在,所以拿到的數(shù)據(jù)源還是a,這時(shí)候就找不到b庫(kù)里的表了。
解決方法
把這三個(gè)入庫(kù)操作分為3個(gè)獨(dú)立的方法,并且都加上@Transactional和 @DS注解(在service上加)。ps:在完成了aMapper.save()之后去調(diào)用bMapper.save()時(shí),一定要把@Transactional設(shè)置為Propagation.REQUIRES_NEW,這樣在調(diào)用另一個(gè)事務(wù)方法時(shí),TransactionInterceptor 會(huì)將原事務(wù)掛起,暫時(shí)性的將原事務(wù)信息和當(dāng)前線程解綁。
pps:
在一個(gè)事務(wù)方法里用this來調(diào)用另一個(gè)事務(wù)方法時(shí),@DS也會(huì)起作用,原因是this調(diào)用的不是事務(wù)對(duì)象,不會(huì)開啟事務(wù)。想具體了解可以看我之前發(fā)的這篇文章 //www.dbjr.com.cn/article/222082.htm
動(dòng)態(tài)數(shù)據(jù)源切換失敗
由事務(wù)@Transactional注解導(dǎo)致動(dòng)態(tài)數(shù)據(jù)源切換失效的問題
不多BB,直接上代碼:
public class DataSourceKey { /** * 用戶數(shù)據(jù)源 */ public final static String USER = "userDataSource"; /** * 報(bào)表數(shù)據(jù)源 */ public final static String REPORT = "reportDataSource"; /** * 所有數(shù)據(jù)源的集合 */ final static List<String> SOURCES = ImmutableList.of(USER, REPORT); /** * 根據(jù)包名找到數(shù)據(jù)源, 多數(shù)據(jù)源的前綴不能存在相同的。 例: user -> userDataSource * * @param pack 包名 * @return 數(shù)據(jù)源名 */ public static String getDataSourceKey(String pack) { return SOURCES.stream().filter(s -> s.startsWith(pack)).findFirst().orElse(USER); } }
@Component @Aspect @Order(-1) @Slf4j public class DynamicDataSourceAspect { @Pointcut("execution(* com.in.g.data.mapper..*.*(..))") public void dataSourcePointcut() { } @Before("dataSourcePointcut()") public void doBefore(JoinPoint point) throws Throwable { log.debug("切換數(shù)據(jù)源開始。。。。。。。。。。。。"); Package pack = point.getSignature().getDeclaringType().getPackage(); String str = StringUtils.substringAfterLast(pack.getName(), "."); String dataSourceKey = DataSourceKey.getDataSourceKey(str); DynamicDataSourceHolder.set(dataSourceKey); log.debug("切換數(shù)據(jù)源成功,當(dāng)前數(shù)據(jù)源:{}", dataSourceKey); } @After("dataSourcePointcut()") public void doAfterReturning() throws Throwable { DynamicDataSourceHolder.clear(); } }
/** * 動(dòng)態(tài)數(shù)據(jù)源持有者 */ public class DynamicDataSourceHolder { public static ThreadLocal<String> keyHolder = new ThreadLocal<>(); public static void clear() { keyHolder.remove(); } public static void set(String key) { keyHolder.set(key); } public static String get() { return keyHolder.get(); } }
/** * 動(dòng)態(tài)數(shù)據(jù)源配置 */ public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.get(); } }
//正確的代碼 --這里偷懶了,直接controller調(diào)用dao層 @GetMapping("/testdb") public String testDateSources(){ //縮小事務(wù)的范圍 add(); rptFieldMapper.selectxxxx(); return "sss"; } @Transactional(rollbackFor = Exception.class) public void add() { userMapper.insertXXXX(xxxx); }
//錯(cuò)誤的代碼,@Transactional注解會(huì)導(dǎo)致 數(shù)據(jù)源切換失敗 @GetMapping("/testdb") @Transactional(rollbackFor = Exception.class) public String testDateSources(){ userMapper.insertXXXX(xxxx); rptFieldMapper.selectXXXX(); return "sss"; }
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Boot 2.2 正式發(fā)布,大幅性能提升 + Java 13 支持
隨著 Spring Framework 5.2.0 成功發(fā)布之后,Spring Boot 2.2 也緊跟其后,發(fā)布了第一個(gè)版本:2.2.0。下面就來一起來看看這個(gè)版本都更新了些什么值得我們關(guān)注的內(nèi)容2019-10-10Java實(shí)現(xiàn)簡(jiǎn)單控制臺(tái)版ATM系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)簡(jiǎn)單控制臺(tái)版ATM系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05怎么把本地jar包放入本地maven倉(cāng)庫(kù)和遠(yuǎn)程私服倉(cāng)庫(kù)
這篇文章主要介紹了怎么把本地jar包放入本地maven倉(cāng)庫(kù)和遠(yuǎn)程私服倉(cāng)庫(kù)的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06nacos中的配置使用@Value注解獲取不到值的原因及解決方案
這篇文章主要介紹了nacos中的配置使用@Value注解獲取不到值的原因分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03SpringBoot實(shí)現(xiàn)接口防刷的兩種方法
接口被刷指的是同一接口被頻繁調(diào)用,可能是由于以下原因?qū)е拢簮阂夤艉驼`操作或程序錯(cuò)誤,本文給大家介紹了SpringBoot實(shí)現(xiàn)接口防刷的兩種方法,并有相關(guān)的代碼示例供大家參考,需要的朋友可以參考下2024-06-06