【java】@Transactional導致@DS注解切換數據源失效

??最近業務中出現了多商戶多租戶的邏輯,所以需要分庫,項目框架使用了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種解決方案。

  1. 在Service類或者方法上添加@DS注解
  2. 在調用帶有Transactional注解的方法前切換數據庫
  3. 自己實現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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/896652.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/896652.shtml
英文地址,請注明出處:http://en.pswp.cn/news/896652.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

淺入淺出Selenium DevTools

前言 在自動化測試領域&#xff0c;Selenium一直是主流工具之一。隨著前端技術的不斷發展&#xff0c;瀏覽器的功能也在不斷豐富。 Selenium 3版本前&#xff0c;一套通用的采集流程如上圖所示&#xff1a; 打開Charles&#xff0c;設置Session自動導出頻次及導出路徑Seleniu…

04 路由表的IP分組傳輸過程

目錄 1、路由表的核心結構 2、IP分組傳輸過程和數據包轉發過程 2.1、IP分組傳輸過程 2.2、數據包轉發過程 2.3、IP分組傳輸過程和數據包轉發的區別 3、數據包的變化 3.1、拓撲結構 3.2、傳輸過程詳解&#xff08;主機A → 主機B&#xff09; 3.2.1、主機A發送數據 3.2…

【子網掩碼計算器:Python + Tkinter 實現】

子網掩碼計算器&#xff1a;Python Tkinter 實現 引言代碼功能概述代碼實現思路1. 界面設計2. 功能實現3. 事件處理 子網掩碼計算器實現步驟1. 導入必要的庫2. 定義主窗口類 SubnetCalculatorApp3. 創建菜單欄4. 創建界面組件5. 判斷 IP 地址類別6. 計算子網信息7. 其他功能函…

【練習】【貪心】力扣1005. K 次取反后最大化的數組和

題目 1005 K 次取反后最大化的數組和 給你一個整數數組 nums 和一個整數 k &#xff0c;按以下方法修改該數組&#xff1a; 選擇某個下標 i 并將 nums[i] 替換為 -nums[i] 。 重復這個過程恰好 k 次。可以多次選擇同一個下標 i 。 以這種方式修改數組后&#xff0c;返回數組 可…

3dsmax中使用python創建PBR材質并掛接貼圖

前言 筆者處理模型時下載到一個pbr材質庫貼圖包&#xff0c;手動每次創建材質過于麻煩&#xff0c;因此計劃使用自動化腳本根據貼圖名自動創建材質。 3dsmax的原本腳本使用的是maxscript&#xff0c;語法有點奇怪懶得學&#xff0c;發現也支持使用python編寫腳本&#…

Metal學習筆記九:光照基礎

光和陰影是使場景流行的重要要求。通過一些著色器藝術&#xff0c;您可以突出重要的對象、描述天氣和一天中的時間并設置場景的氣氛。即使您的場景由卡通對象組成&#xff0c;如果您沒有正確地照亮它們&#xff0c;場景也會變得平淡無奇。 最簡單的光照方法之一是 Phong 反射模…

JAVA學習筆記038——bean的概念和常見注解標注

什么是bean? Bean 就是 被 Spring 管理的對象&#xff0c;就像工廠流水線上生產的“標準產品”。這些對象不是你自己 new 出來的&#xff0c;而是由 Spring 容器&#xff08;一個超級工廠&#xff09;幫你創建、組裝、管理。 由 Component、Service、Controller 等注解標記的…

start DL from stratch (2)!!!

start DL from stratch &#xff08;2&#xff09;!!! 一、CPU and GPUcpuGPU安培架構愛達洛夫萊斯架構 二、使用conda創建一個新的虛擬環境三、autodl操作先知Linux復習目錄文件和數據上傳對于整個鏡像的操作守護進程Tips 四、autodl租用創建實例<big>沒有所需要的版本的…

機器學習:線性回歸,梯度下降

線性回歸模型 (Linear Regression Model) 梯度下降算法 (Gradient Descent Algorithm) 的數學公式

論文筆記-NeurIPS2017-DropoutNet

論文筆記-NeurIPS2017-DropoutNet: Addressing Cold Start in Recommender Systems DropoutNet&#xff1a;解決推薦系統中的冷啟動問題摘要1.引言2.前言3.方法3.1模型架構3.2冷啟動訓練3.3推薦 4.實驗4.1實驗設置4.2在CiteULike上的實驗結果4.2.1 Dropout率的影響4.2.2 實驗結…

nvm的學習

學習 nvm&#xff08;Node Version Manager&#xff09; 是掌握 Node.js 開發的關鍵技能之一。以下是系統的學習路徑和實戰指南&#xff0c;涵蓋從基礎到進階的內容&#xff1a; 一、基礎入門 1. nvm 的核心作用 多版本共存&#xff1a;安裝和管理多個 Node.js 版本&#xff…

GPT-4.5實際性能評測:實際探索

摘要 經過數萬輪嚴格測試&#xff0c;GPT-4.5的性能并未超越其前代產品GPT-4。此前發布的《GPT-4.5 一手實測&#xff1a;垃圾》一文中存在不準確描述&#xff0c;在此向讀者致歉。盡管GPT-4.5在價格上有所提升且響應速度較慢&#xff0c;但測試結果顯示其模型素質并未達到預期…

從UNIX到Linux:操作系統進化史與開源革命

從UNIX到Linux&#xff1a;操作系統進化史與開源革命 一、操作系統&#xff1a;數字世界的基石 1.1 什么是操作系統&#xff1f; 操作系統&#xff08;OS&#xff09;是計算機系統的核心管理者&#xff0c;承擔著三大核心使命&#xff1a; 硬件指揮官&#xff1a;直接管理C…

如何修改安全帽/反光衣檢測AI邊緣計算智能分析網關V4的IP地址?

TSINGSEE青犀推出的智能分析網關V4&#xff0c;是一款集成了BM1684芯片的高性能AI邊緣計算智能硬件。其內置的高性能8核ARM A53處理器&#xff0c;主頻可高達2.3GHz&#xff0c;INT8峰值算力更是達到了驚人的17.6Tops。此外&#xff0c;該硬件還預裝了近40種AI算法模型&#xf…

【全棧開發】----Mysql基本配置與使用

本篇是在已下載Mysql的情況下進行的&#xff0c;若還未下載或未創建Mysql服務&#xff0c;請轉到這篇: 2024 年 MySQL 8.0.40 安裝配置、Workbench漢化教程最簡易&#xff08;保姆級&#xff09;_mysql8.0.40下載安裝教程-CSDN博客 本文對于mysql的操作均使用控制臺sql原生代碼…

C++ primer plus 第四節 復合類型

本章內容包括: ? 創建和使用數組 ? 創建和使用 c-風格字符串 ? 創建和使用 string 類字符串 ? 使用方法getline( )和 get( )讀取字符串 ? 混合輸入字符串和數字 ? 創建和使用結構 ? 創建和使用共用休 ? 創建和使用枚舉 ? 創建和使用指針 ? 使用 new和delete 管理動態…

Java中的泛型類 --為集合的學習做準備

學習目標 ● 掌握在集合中正確使用泛型 ● 了解泛型類、泛型接口、泛型方法 ● 了解泛型上下限 ● 了解基本的使用場景 1.有關泛型 1.1泛型的概念 泛型&#xff08;Generics&#xff09;是Java中引入的參數化類型機制&#xff0c;允許在定義類、接口或方法時使用類型參數&a…

VUE3+Vite使用TailwindCSS【若依前后端分離框架】

參考&#xff1a;https://tailwind.nodejs.cn/docs/guides/vite#vue 和 https://blog.csdn.net/hjl_and_djj/article/details/144694485依次運行命令&#xff1a; cnpm install -D tailwindcss3.4.17 postcss autoprefixernpx tailwindcss init -p修改配置文件tailwind.config.…

FFmpeg入門:最簡單的音頻播放器

FFmpeg入門&#xff1a;最簡單的音頻播放器 歡迎大家來到FFmpeg入門的第二章&#xff0c;今天只做一個最簡單的FFmpeg音頻播放器&#xff1b;同樣&#xff0c;話不多說&#xff0c;先上流程圖 流程圖 以上流程和視頻播放器的解碼過程基本上是一致的&#xff1b; 不同點在于 S…

在Ubuntu下,源碼編譯安裝Python

在Ubuntu下&#xff0c;源碼編譯安裝Python 知識點 知識點1&#xff1a;在 Linux 系統里&#xff0c;/usr 目錄通常用于存放一些共享的、只讀的程序和數據&#xff0c;是系統安裝軟件的一個重要位置。而 /usr/src 目錄一般是用來存放系統源代碼以及一些軟件包的源代碼的地方 …