1. 程式人生 > >事務@Transactional和非同步@Async註解失效

事務@Transactional和非同步@Async註解失效

問題場景重現

場景一:

Spring的非同步執行註解@Async,在呼叫這個方法的時候發現,不對勁,耗時的邏輯我已經加入到非同步取做了,怎麼介面請求的響應這麼慢,趕緊看日誌,懵X,加了非同步註解,卻沒有非同步執行。

場景二:

在專案中用到@Transactional註解實現事務是必須滴,如果你還在用xml配置,那我只能說……。 

但是有時候我們會發現在方法上加了@Transactional註解卻出現靈異事件,在方法內出現異常,資料還是插入到資料庫,沒有回滾,事務哪裡去了,明明是加了的。

@Async註解實現原因分析和解決方案

在看下面的內容之前,對動態代理不是很熟悉的可以看一下我之前的一篇文章(

靜態代理、動態代理,以及動態代理的呼叫說明)。 

這裡新增的註解是通過Spring AOP對方法的一種增強,而Spring AOP的原理就是動態代理,他的代理有兩種,分別是CGLB和JDK自帶的代理,Spring AOP會根據具體的實現不同,採用不同的代理方式。 

動態代理的原理了解了,下面的問題就可以很好的理解。

非同步測試介面

public interface AsyncAopService { void addOrder(); void sendMsg(int result); }

非同步測試介面實現

@Service @Slf4j public class AsyncAopServiceImpl implements AsyncAopService { @Autowired private OrderDao orderDao; @Autowired private MsgDao msgDao; /** * 新增訂單後會給使用者非同步的推送資訊 */ @Transactional //這裡為了讓該被代理,加此註解 public void addOrder() { int result = orderDao.insert(OrderModel.builder() .amount(10000L) .orderId("ORDER_2018042601") .phone("15600001212") .userId("U_001") .build()); String currentThreadName = Thread.currentThread().getName(); sendMsg(result); System.out.println(currentThreadName + "------>下單結束:mark"); } @Async public void sendMsg(int result) { try { Thread thread = Thread.currentThread(); thread.sleep(3000);//停留3秒 String currentThreadName = thread.getName(); if (result == 1) { msgDao.insert(MsgModel.builder().msgContent("下單成功!").receiver("15600001212").build()); System.out.println(currentThreadName + "------>傳送資訊成功"); } else { msgDao.insert(MsgModel.builder().msgContent("下單失敗!").receiver("15600001212").build()); System.out.println(currentThreadName + "------>傳送資訊失敗"); } } catch (Exception e) { e.printStackTrace(); } } }

測試類

@Test public void AsyncTest() throws InterruptedException { System.out.println("=======async test start======="); asyncAopService.addOrder(); System.out.println("=======async test end======="); /** * 在這裡讓執行緒睡5秒的原因是為了能夠看到非同步執行的結果日誌 * 小知識點:在Junit測試中,如果主執行緒執行結束,整個測試過程也結束了,存在的非同步邏輯如果沒有執行完就不會執行啦! * 測試方式:把這行程式碼去掉,執行測試,t_order_info表中會插入資料,但是t_msg_info表中無資料插入。 */ Thread.sleep(5000); }

測試類上的註解:

@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

Spring Boot啟動類的註解:

@SpringBootApplication @MapperScan("com.minuor.aop.dao") @EnableAspectJAutoProxy @EnableAsync //開啟非同步功能

測試結果

=======async test start======= main------>傳送資訊成功 main------>下單結束:mark =======async test end=======

執行的過程很慢,兩行日誌的執行緒名稱相同,並且mark日誌是在傳送資訊成功後再輸出的,回到程式碼可以知道傳送資訊邏輯是非同步的執行,為什麼會和下單過程的執行緒名稱相同,並且非同步執行傳送訊息是延遲的,為何日誌還在mark日誌前。種種跡象表明,這不是一個非同步執行,而是順序執行。但是這裡是加了非同步的註解的,實際沒有生效。 

在addOrder()方法裡面直接呼叫sendMsg(……)方法,這裡還隱含一個關鍵字,那就是this,實際上這裡呼叫是這樣的:this.sendMsg(),this是當前物件。而addOrder()是被代理的,在代理物件中執行結束增強後,通過invoke,用實際AsyncAopServiceImpl物件來呼叫addOrder()方法執行業務邏輯。在業務邏輯內又呼叫了sendMsg(……)方法,呼叫的物件是當前物件,當前物件是AsyncAopServiceImpl,問題就出在這裡,因為要想用非同步執行sendMsg(……),必須用代理物件執行,因為代理物件要做非同步相關的增強,但是此時卻直接用AsyncAopServiceImpl物件呼叫,繞過了代理物件增強的部分,也就是說代理增強部分失效,@Async註解失效。原來想非同步執行的邏輯,變成了順序執行。

解決方案

沒有用代理物件執行sendMsg(……),被AsyncAopServiceImpl物件搶佔了先機。那麼解決就是要讓代理物件來執行sendMsg(……)。

在呼叫sendMsg(……)之前新增下面的程式碼

AsyncAopService service = (AsyncAopService) AopContext.currentProxy(); //獲取代理物件 service.sendMsg(result); //通過代理物件呼叫sendMsg,做非同步增強

這裡還不算完,如果就這樣執行,那肯定會報錯。

在@EnableAspectJAutoProxy新增屬性值。

@EnableAspectJAutoProxy(exposeProxy = true)

執行結果

=======async test start======= main------>下單結束:mark =======async test end======= SimpleAsyncTaskExecutor-1------>傳送資訊成功

結果也是想要的結果,下單結束,整個測試結束,在測試結束後等待5秒,等待非同步日誌列印。主執行緒和非同步執行緒是不同的兩個。 

如果對代理物件和當前物件有點懵的話,可以加上下面的兩行程式碼:

System.out.println("------>代理物件:"+service.getClass()); System.out.println("------>當前物件:"+this.getClass());

  • 1
  • 2

得到的結果:

------>代理物件:class com.minuor.aop.impl.AsyncAopServiceImpl$$EnhancerBySpringCGLIB$$9de92f4b //可以看出來是CGLB動態代理 ------>當前物件:class com.minuor.aop.impl.AsyncAopServiceImpl

  • 1
  • 2

@Transactional註解失效的原因分析

這個原因和上面的是相同的,代理被繞過,直接當前物件執行應該被增強的方法,導致方法沒有被增強成功。但是可以說一下兩個情況。

情況一:在非代理增強方法中呼叫加了@Transactional增強的方法

這個過程容易理解,不解釋。

業務程式碼

@Service public class TransactionalAopServiceImpl implements TransactionalAopService { @Autowired private OrderDao orderDao; @Autowired private UserDao userDao; public void addOrder() { orderDao.insert(OrderModel.builder() .userId("YK_002") //遊客編號 .phone("13522203330") .orderId("ORDER_2018042602") .amount(10000L) .build()); //默開使用者 System.out.println("--->"+this.getClass()); addUser("13522203330"); } @Transactional public void addUser(String phone) { userDao.insert(UserModel.builder().userName("zhangsan").userPhone(phone).build()); throw new RuntimeException(); } }

執行結果是order訂單資訊新增成功,同時user使用者資訊也新增成功,資料庫都有資料,沒有回滾。按照表面理解應該是order新增成功,user新增失敗,因為addUser上加了事務,會回滾。原理參照@Async失效的原理解釋。

情況二:addOrder和addUser方法上都新增@Transactional

這種情況下,是可以回滾的,但是不太清楚是在哪個事務回滾,也不太清楚@Transactional是都有效,還是其中一個有效。但是可以模擬,那就是定義三個異常,分別是OrderException、UserException、OtherException,然後在兩個方法上指定回滾異常類。通過丟擲不同的異常來看具體的結果。

@Transactional修改

@Transactional(rollbackFor = OrderException.class, noRollbackFor = RuntimeException.class) //addOrder方法上 @Transactional(rollbackFor = UserException.class, noRollbackFor = RuntimeException.class) //addUser方法上

  • 1
  • 2

執行結果分析

1、拋OtherException異常,沒有回滾,order、user資料都成功錄入到資料庫中; 

2、拋UserException異常,沒有回滾,order、user資料都成功錄入到資料庫中,這裡可以看的出來addUser方法上的@Transactional註解是無效的; 

3、拋OrderException異常,回滾成功,order、user資料都沒有錄入到資料庫中,addOrder方法上的@Transactional有效。 

這樣的結果加上動態代理原理的分析不難得出結果,addUser方法的代理增強被繞過,只是普通的一個方法呼叫,而且這個方法是包含在addOrder方法事務內的。

綜合解決方案

沒有用代理物件執行sendMsg(……),被AsyncAopServiceImpl物件搶佔了先機。那麼解決就是要讓代理物件來執行sendMsg(……)。

在呼叫sendMsg(……)之前新增下面的程式碼

AsyncAopService service = (AsyncAopService) AopContext.currentProxy(); //獲取代理物件 service.sendMsg(result); //通過代理物件呼叫sendMsg,做非同步增強

這裡還不算完,如果就這樣執行,那肯定會報錯。

在@EnableAspectJAutoProxy新增屬性值。

@EnableAspectJAutoProxy(exposeProxy = true)