javascript
Spring @Transactional踩坑记
@Transactional踩坑記
總述
? Spring在1.2引入@Transactional注解, 該注解的引入使得我們可以簡(jiǎn)單地通過(guò)在方法或者類上添加@Transactional注解,實(shí)現(xiàn)事務(wù)控制。 然而看起來(lái)越是簡(jiǎn)單的東西,背后的實(shí)現(xiàn)可能存在很多默認(rèn)規(guī)則和限制。而對(duì)于使用者如果只知道使用該注解,而不去考慮背后的限制,就可能事與愿違,到時(shí)候線上出了問(wèn)題可能根本都找不出啥原因。
踩坑記
1. 多數(shù)據(jù)源
事務(wù)不生效
背景介紹
? 由于數(shù)據(jù)量比較大,項(xiàng)目的初始設(shè)計(jì)是分庫(kù)分表的。于是在配置文件中就存在多個(gè)數(shù)據(jù)源配置。大致的配置類似下面:
<!-- 數(shù)據(jù)源A和事務(wù)配置 --> <bean id="dataSourceA" class="com.mchange.v2.c3p0.ComboPooledDataSource"destroy-method="close">.... </bean><bean id="dataSourceTxManagerA"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSourceA" /> </bean> <!-- mybatis自動(dòng)掃描生成Dao類的代碼 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><property name="annotationClass" value="com.rampage.mybatis.annotation.mapperDao" /><property name="nameGenerator" ref="sourceANameGenerator" /><property name="sqlSessionFactoryBeanName" value="sourceAsqlSessionFactory" /><property name="basePackage" value="com.rampage" /> </bean> <!-- 自定義的Dao名稱生成器,prefix屬性指定在bean名稱前加上對(duì)應(yīng)的前綴生成Dao --> <bean id="sourceANameGenerator" class="com.rampage.mybatis.dao.namegenerator.MyNameGenerator"><property name="prefix" value="sourceA" /> </bean><bean id="sourceAsqlSessionFactory" class="org.mybatis.spring.PathSqlSessionFactoryBean"><property name="dataSource" ref="dataSourceA" /> </bean><!-- 數(shù)據(jù)B和事務(wù)配置 --> <bean id="dataSourceB" class="com.mchange.v2.c3p0.ComboPooledDataSource"destroy-method="close">.... </bean><bean id="dataSourceTxManagerB"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSourceB" /> </bean>? 但是在實(shí)際部署的時(shí)候,因?yàn)槭菃螜C(jī)部署的,多個(gè)數(shù)據(jù)源實(shí)際上對(duì)應(yīng)的是同一個(gè)庫(kù),不存在分布式事務(wù)的問(wèn)題。所以在代碼編寫(xiě)的時(shí)候,直接通過(guò)在@Transactional注解來(lái)實(shí)現(xiàn)事務(wù)。具體代碼樣例大致如下:
@Service public class UserService {@Resource("sourceBUserDao") // 其實(shí)這時(shí)候Dao對(duì)應(yīng)的是sourceBprivate UserDao userDao;@Transactionalpublic void update(User user) {userDao.update(user);// Other db operations...} }? 這中寫(xiě)法的代碼一直在線上運(yùn)行了一兩年,沒(méi)有出過(guò)啥問(wèn)題.....反而是我在做一個(gè)需求的時(shí)候,考慮到@Transactional注解里面的 數(shù)據(jù)庫(kù)操作,如果沒(méi)有同時(shí)成功或者失敗的話,數(shù)據(jù)會(huì)出現(xiàn)混亂的情況。于是自己測(cè)試了一下,開(kāi)啟了這段踩坑之旅.....
原因分析:
? 開(kāi)始在網(wǎng)上搜了一下Transactional注解不支持多數(shù)據(jù)源, 于是我當(dāng)時(shí)把所有數(shù)據(jù)庫(kù)操作都采用sourceB作為前綴的Dao進(jìn)行操作。結(jié)果測(cè)試一遍發(fā)現(xiàn)還是沒(méi)有事務(wù)效果。沒(méi)有什么是源碼解決不了的,于是就開(kāi)始debug源碼,發(fā)現(xiàn)最終啟動(dòng)的事務(wù)管理器竟然是dataSourceTxManagerA。 難道和事務(wù)管理器聲明的順序有關(guān)?于是我調(diào)整了下xml配置文件中,事務(wù)管理器聲明的順序,發(fā)現(xiàn)事務(wù)生效了,因此得證。
? 具體來(lái)說(shuō)原因有以下兩點(diǎn):
- @Transactional注解不支持多數(shù)據(jù)源的情況
- 如果存在多個(gè)數(shù)據(jù)源且未指定具體的事務(wù)管理器,那么實(shí)際上啟用的事務(wù)管理器是最先在配置文件中指定的(即先加載的)
解決辦法:
? 對(duì)于多數(shù)據(jù)下的事務(wù)解決辦法如下:
- 在@Transactional注解添加的方法內(nèi),數(shù)據(jù)庫(kù)更新操作統(tǒng)一使用一個(gè)數(shù)據(jù)源下的Dao,不要出現(xiàn)多個(gè)數(shù)據(jù)源下的Dao的情況
- 統(tǒng)一了方法內(nèi)的數(shù)據(jù)源之后,可以通過(guò)@Transactional(transactionManager = "dataSourceTxManagerB")顯示指定起作用的事務(wù)管理器,或者在xml中調(diào)節(jié)事務(wù)管理器的聲明順序
死循環(huán)問(wèn)題
? 這個(gè)問(wèn)題其實(shí)也是多數(shù)據(jù)源導(dǎo)致的,只是更難分析原因。具體場(chǎng)景是:假設(shè)我的貨倉(cāng)里有1000個(gè)貨物,我現(xiàn)在要給用戶發(fā)貨。每批次只能發(fā)100個(gè)。我的貨物有一個(gè)字段來(lái)標(biāo)識(shí)是否已經(jīng)發(fā)過(guò)了,對(duì)于已經(jīng)發(fā)過(guò)的貨不能重新發(fā)(否則只能哭暈在廁所)!代碼的實(shí)現(xiàn)是外層有一個(gè)while(true)循環(huán)去掃描是否還有未發(fā)過(guò)的貨物,而發(fā)貨作為整體的一個(gè)事務(wù),具體代碼如下:
@Transactional public void deliverGoods(List<Goods> goodsList) { // 傳入的參數(shù)是前面循環(huán)查出來(lái)的未發(fā)貨的100個(gè)貨物,作為一個(gè)批次統(tǒng)一發(fā)貨updateBatchId(goodsList, batchId); // 更新同批次貨物的批次號(hào)字段// do other thingsupdateGoodsStatusByBatchId(batchId, delivered); // 根據(jù)前面更新的批次號(hào)取修改數(shù)據(jù)庫(kù)相關(guān)貨物的發(fā)送狀態(tài)為已發(fā)送 }? 從整體上來(lái)看,這段代碼邏輯上沒(méi)有任何問(wèn)題。實(shí)際運(yùn)行的時(shí)候卻發(fā)現(xiàn)出現(xiàn)了死循環(huán)。還好測(cè)試及時(shí)發(fā)現(xiàn),沒(méi)有最終上線。那么具體原因是咋樣的呢?
? 出現(xiàn)這個(gè)問(wèn)題的時(shí)候,配置文件的配置還是同前面一個(gè)問(wèn)題一樣的配置。即實(shí)際上@Transactional注解默認(rèn)起作用的事務(wù)是針對(duì)dataSourceA的。然后跟進(jìn)updateBatchId方法,發(fā)現(xiàn)其最終調(diào)用的方法采用的Dao是sourceA為前綴的Dao,而updateGoodsStatusByBatchId方法最終調(diào)用的Dao是sourceB為前綴的Dao。細(xì)細(xì)分析,我終于知道為啥了 ?
發(fā)貨方法最終起作用的事務(wù)是針對(duì)sourceA的, 也就是updateBatchId方法實(shí)際上作為一個(gè)事務(wù),他是要在方法執(zhí)行完成之后才提交的
oracle默認(rèn)的事務(wù)隔離級(jí)別是READ_COMMITTED, 所以在updateGoodsStatusByBatchId方法去更新的時(shí)候
其實(shí)還讀取不到對(duì)應(yīng)批次號(hào)的記錄,也就不會(huì)做更新
? 解決辦法這里就不說(shuō)了,最終還是同前面一個(gè)問(wèn)題,或者更新的時(shí)候根據(jù)貨物列表去更新。
2. 內(nèi)部調(diào)用
? 內(nèi)部調(diào)用不生效的問(wèn)題其實(shí)大部分大家都知道。舉一個(gè)簡(jiǎn)單的例子:
? 假設(shè)我有一個(gè)類的定義如下:
@Service public class UserService {public void updateUser(User user){// do somothingupdateWithTransaction(user);}@Transactionalpublic void updateWithTransaction(User user) {} }@Service public class BusinessService {@Autowiredprivate UserService userService;public void doUserUpdate(User user) {// do somothinguserService.updateUser(user);} }? 這種情況下大家都知道事務(wù)最終是不會(huì)生效的。因?yàn)閷?duì)于updateWithTransaction方法是通過(guò)內(nèi)部調(diào)用的,這時(shí)候@Transactional注解壓根就不會(huì)生效。但是有時(shí)候情況并不這么明顯,考慮下面的代碼:
@Service public class UserService extends AbstractUserService {public void updateUser(User user){// do somothingupdateWithTransaction(user);}}public abstract class AbstractUserService {protected abstract void updateUser(User user);@Transactionalpublic void updateWithTransaction(User user) {// do update} }@Service public class BusinessService {public void doUserUpdate(User user) {// do somothingAbstractUserService userService = getUserService(); // 假設(shè)最終得到是UserService類的實(shí)例userService.updateUser(user);} }? 這段代碼初一分析,最終調(diào)用的updateUser方法是UserService的方法, 然后調(diào)用的updateWithTransaction是屬于AbstractUserService類的。 好像是調(diào)用的不是同一個(gè)類的方法,按道理事務(wù)應(yīng)該是可以生效的。其實(shí)并沒(méi)有..... 原因其實(shí)還是是內(nèi)部調(diào)用。其實(shí)這種場(chǎng)景我也是在項(xiàng)目中發(fā)現(xiàn)的(坑太多),當(dāng)時(shí)的代碼比這個(gè)復(fù)雜的多,Abstract類包含了一堆可以被子類重寫(xiě)的方法。原來(lái)的代碼大致如下:
public class AbstractService {// 被外部調(diào)用的方法public void outMethod() {if (A) {transactionalMethod1();} else if (B) {transactionalMethod2();} else {transactionalMethod3();} }@Transactionalpublic void transactionalMethod1() {// do something}@Transactionalpublic void transactionalMethod2() {// do something}@Transactionalpublic void transactionalMethod3() {// do something} }? 其中三個(gè)事務(wù)方法都可能被子類重寫(xiě),修改必須兼容老代碼。思考了兼容和接口改造的方式,我最終實(shí)現(xiàn)如下:
public class AbstractService implements TransactionIntf {@Autowiredprivate TransactionService transactionService;public void outMethod() {transactionService.setTransactionIntf(this); // 最終數(shù)據(jù)庫(kù)服務(wù)類注冊(cè)為當(dāng)前實(shí)現(xiàn)類transactionService.processIntransaction(); // 調(diào)用數(shù)據(jù)庫(kù)操作}@Overridepublic void transactionalMethod1() {// do something}@Overridepublic void transactionalMethod2() {// do something}@Overridepublic void transactionalMethod3() {// do something} }public interface TransactionIntf {void transactionalMethod1();void transactionalMethod2();void transactionalMethod3(); }@Service public class TransactionService {// 定義局部線程變量,存儲(chǔ)對(duì)應(yīng)的服務(wù)ThreadLocal<TransactionIntf> serviceLocal = new ThreadLocal<TransactionIntf>();@Transactional // 在此處加注解public void processIntransaction() {try {if (A) {serviceLocal.get().transactionalMethod1();} else if (B) {serviceLocal.get().transactionalMethod2();} else {serviceLocal.get().transactionalMethod3();} } finally {// 最終在本地線程局部變量移除serviceLocal.remove();}}// 設(shè)置服務(wù)到本地線程局部變量public void setTransactionIntf(TransactionIntf service) {this.serviceLocal.set(service);} }填坑總結(jié)
? 下面直接給出網(wǎng)站上關(guān)于@Transactional使用的注意點(diǎn):
- @Transactional annotations only work on public methods. If you have a private or protected method with this annotation there’s no (easy) way for Spring AOP to see the annotation. It doesn’t go crazy trying to find them so make sure all of your annotated methods are public.
- Transaction boundaries are only created when properly annotated (see above) methods are called through a Spring proxy. This means that you need to call your annotated method directly through an @Autowired bean or the transaction will never start. If you call a method on an @Autowired bean that isn’t annotated which itself calls a public method that is annotated YOUR ANNOTATION IS IGNORED. This is because Spring AOP is only checking annotations when it first enters the @Autowired code.
- Never blindly trust that your @Transactional annotations are actually creating transaction boundaries. When in doubt test whether a transaction really is active (see below)
? 另外附上驗(yàn)證是否開(kāi)啟的工具類源碼(我只是搬運(yùn)工):
class TransactionTestUtils {private static final boolean transactionDebugging = true;private static final boolean verboseTransactionDebugging = true;public static void showTransactionStatus(String message) {System.out.println(((transactionActive()) ? "[+] " : "[-] ") + message);}// Some guidance from: http://java.dzone.com/articles/monitoring-declarative-transac?page=0,1public static boolean transactionActive() {try {ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();Class tsmClass = contextClassLoader.loadClass("org.springframework.transaction.support.TransactionSynchronizationManager");Boolean isActive = (Boolean) tsmClass.getMethod("isActualTransactionActive", null).invoke(null, null);return isActive;} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalArgumentException e) {e.printStackTrace();} catch (SecurityException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();}// If we got here it means there was an exceptionthrow new IllegalStateException("ServerUtils.transactionActive was unable to complete properly");}public static void transactionRequired(String message) {// Are we debugging transactions?if (!transactionDebugging) {// No, just returnreturn;}// Are we doing verbose transaction debugging?if (verboseTransactionDebugging) {// Yes, show the status before we get to the possibility of throwing an exceptionshowTransactionStatus(message);}// Is there a transaction active?if (!transactionActive()) {// No, throw an exceptionthrow new IllegalStateException("Transaction required but not active [" + message + "]");}} }參考鏈接
http://blog.timmattison.com/archives/2012/04/19/tips-for-debugging-springs-transactional-annotation/
轉(zhuǎn)載于:https://www.cnblogs.com/Kidezyq/p/8541199.html
總結(jié)
以上是生活随笔為你收集整理的Spring @Transactional踩坑记的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: git diff命令详解
- 下一篇: UVa 208 - Firetruck