被標記爲事務的方法互相調用的坑(上)

相信你們必定用過Spring中的註解型事務,配合上Spring Boot,只須要在方法上打一個@Transactional 就能夠完成,真香。spring

可是若是你們對其中的機制只知其一;不知其二的話,可能一不當心就會掉進坑,而後久久沒法爬出來。app

下面我就分享下 被標記爲事務的方法互相調用的坑分佈式

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。ide

首先我寫兩個事務方法:微服務

 
@Autowired AccountMapper mapper; @Transactional @Override public void insertCodeBear() { Account account = new Account(); account.setAccount("CodeBear"); account.setPassword("CodeBear"); mapper.insert(account); } @Transactional @Override public void insertCodeMonkey() { Account account = new Account(); account.setAccount("CodeMonkey"); account.setPassword("CodeMonkey"); mapper.insert(account); }

如今我想在insertCodeBear方法裏面調用insertCodeMonkey方法,可是insertCodeMonkey不是很重要,就算失敗,也不能影響到insertCodeBear方法的執行,可是insertCodeMonkey該回滾的仍是要回滾,咱們很容易寫出以下代碼:源碼分析

 
@Autowired AccountMapper mapper; @Transactional @Override public void insertCodeBear() { try { insertCodeMonkey(); } catch (Exception ex) { } Account account = new Account(); account.setAccount("CodeBear"); account.setPassword("CodeBear"); mapper.insert(account); } @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void insertCodeMonkey() { Account account = new Account(); account.setAccount("CodeMonkey"); account.setPassword("CodeMonkey"); mapper.insert(account); int a = 1 / 0;//自殺代碼,便於測試 }

在第二個方法中,用了自殺代碼,便於測試。性能

看上去一點問題都沒有:第一個方法會成功,第二個方法會失敗而且回滾。可是僅僅是看上去,當咱們運行一下,會發現奇怪的事情發生了:學習

 

兩個方法居然都成功了!!Why?測試

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。this

爲了排查問題,須要開啓一下 有關事務 的日誌,在 配置文件 中加上下面的配置:

 
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=debug

而後運行,看下控制檯打印的內容:

 

圖片可能有點模糊,你們能夠在新標籤頁中打開這圖片,能夠看到這裏分明只開了一個事務,並且事務的傳播行爲是PROPAGATION_REQUIRED,這是事務的默認傳播行爲,也就是這裏只開啓了insertCodeBear方法的事務,並無開啓insertCodeMonkey的事務。

這是什麼緣由?爲了更好的說明問題產生的緣由,我須要手寫一個AOP。

在此以前你們要達成一個共識,@Transactional 其實也是經過AOP去實現的。

 

AOP有幾種實現方式,我這裏採用JDK動態代理的方式:

代碼入口:

 
public class Main { public static void main(String[] args) { BookServiceImpl impl = new BookServiceImpl(); InvocationHandler myInvocationHandler = new MyInvocationHandler(impl); Object o = Proxy.newProxyInstance(myInvocationHandler.getClass().getClassLoader(), impl.getClass().getInterfaces(), myInvocationHandler); ((IBookService) o).add(); } }

接口:

 
public interface IBookService { void add(); void delete(); }

實現類:

 
public class BookServiceImpl implements IBookService { public void add() { delete(); System.out.println("add"); } public void delete() { System.out.println("delete"); } }

切面定義:

 
public class MyInvocationHandler implements InvocationHandler { private Object obj; public MyInvocationHandler(Object obj) { this.obj = obj; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("開始啦,小夥子"); method.invoke(obj, args); System.out.println("結束啦,小夥子"); return null; } }

在Main入口裏面調用了實現類的代理對象,調用了add方法,add方法裏面又調用了delete的方法。很簡單吧。按照咱們的想法,應該是打印出兩次 切面中定義的話,可是事實是 只打印了一次:

 

讓咱們在切面方法中加上這行代碼:

 
System.out.println("方法是" + method.getName());

看看是哪一個方法進入到了這裏。

運行:

 

add方法進入到了這裏,可是delete方法卻沒有進來。

讓咱們再回到第一個例子,爲了讓你們看的清楚一點,我再貼上insertCodeBear被調用的代碼:

 
@RestController @RequestMapping("/CodeBear") public class HelloWorldController { @Autowired AccountService service; @GetMapping("/insert") public void insert() { service.insertCodeBear(); } }

AccountService 是一個接口,裏面定義了insertCodeBear和insertCodeMonkey虛方法。

咱們打一個斷點在

 
service.insertCodeBear();

這裏,而後調試看下service是一個什麼東西:

 

你會發現,service已經不是簡單的AccountService 的實現類了,而是實現類的代理對象,從這裏也能夠看出,其實@Transactional也是經過AOP去實現的。

經過兩個例子,能夠獲得一個結論:只有調用代理對象的方法才能被攔截,因此 在方法A中直接調用方法B,方法B是不會被攔截的

這也就是爲何insertCodeMonkey的事務沒有被開啓的緣由了,由於insertCodeMonkey方法是insertCodeBear直接調用的。

那麼,這個問題該如何解決呢?在下一篇博客,我會採用幾種方式來解決這個問題(這篇博客已經比較長了,由於加上了不少看上去沒什麼用的「廢話」,由於能夠直接寫出結論,而後再寫解決方案就是了。可是我仍是很詳細的,把「廢話」都寫出來了,就是由於分析問題的思路纔是最重要的 )。

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。

相關文章
相關標籤/搜索