1. 程式人生 > >被標記為事務的方法互相呼叫的坑(上)

被標記為事務的方法互相呼叫的坑(上)

相信大家一定用過Spring中的註解型事務,配合上Spring Boot,只需要在方法上打一個@Transactional 就可以完成,真香。

但是如果大家對其中的機制一知半解的話,可能一不小心就會掉進坑,然後久久無法爬出來。

下面我就分享下 被標記為事務的方法互相呼叫的坑

首先我寫兩個事務方法:

    @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;//自殺程式碼,便於測試 } 複製程式碼

在第二個方法中,用了自殺程式碼,便於測試。

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

image.png
兩個方法竟然都成功了!!Why?

為了排查問題,需要開啟一下 有關事務 的日誌,在 配置檔案 中加上下面的配置:

logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=debug
複製程式碼

然後執行,看下控制檯列印的內容:

image.png
圖片可能有點模糊,大家可以在新標籤頁中開啟這圖片,可以看到這裡分明只開了一個事務,而且事務的傳播行為是PROPAGATION_REQUIRED,這是事務的預設傳播行為,也就是這裡只開啟了insertCodeBear方法的事務,並沒有開啟insertCodeMonkey的事務。

這是什麼原因?為了更好的說明問題產生的原因,我需要手寫一個AOP。

在此之前大家要達成一個共識,@Transactional 其實也是通過AOP去實現的。

image.png

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的方法。很簡單吧。按照我們的想法,應該是打印出兩次 切面中定義的話,但是事實是 只打印了一次:

image.png
讓我們在切面方法中加上這行程式碼:

 System.out.println("方法是" + method.getName());
複製程式碼

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

執行:

image.png

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是一個什麼東西:

image.png

你會發現,service已經不是簡單的AccountService 的實現類了,而是實現類的代理物件,從這裡也可以看出,其實@Transactional也是通過AOP去實現的。

通過兩個例子,可以得到一個結論:只有呼叫代理物件的方法才能被攔截,所以 在方法A中直接呼叫方法B,方法B是不會被攔截的

這也就是為什麼insertCodeMonkey的事務沒有被開啟的原因了,因為insertCodeMonkey方法是insertCodeBear直接呼叫的。

那麼,這個問題該如何解決呢?在下一篇部落格,我會採用幾種方式來解決這個問題(這篇部落格已經比較長了,因為加上了很多看上去沒什麼用的“廢話”,因為可以直接寫出結論,然後再寫解決方案就是了。但是我還是很詳細的,把“廢話”都寫出來了,就是因為分析問題的思路才是最重要的 )。