1.前言
一道非常有意思的面試題目。大概是這樣子的,如果在一個事務中,開啟執行緒進行插入更新等操作,如果報錯了,事務是否會進行回滾
2.程式碼
示例1
@RequestMapping("/test/publish/submit")
public String testPublish1() {
log.info("start...");
transactionTemplate.execute(new TransactionCallback<String>() {
@Override
public String doInTransaction(TransactionStatus status) {
TElement element = new TElement();
element.setfElementId(10L);
element.setfElementName("111");
mapper.insertSelective(element);
element = new TElement();
element.setfElementId(10L);
element.setfElementName("222");
mapper.insertSelective(element);
return "OK";
}
});
log.info("end...");
return "ok";
}
示例2
@RequestMapping("/test/publish/submit2")
public String testPublish2() {
log.info("start...");
transactionTemplate.execute(new TransactionCallback<String>() {
@Override
public String doInTransaction(TransactionStatus status) {
es.submit(() -> {
TElement element = new TElement();
element.setfElementId(10L);
element.setfElementName("111");
mapper.insertSelective(element);
});
es.submit(() -> {
TElement element = new TElement();
element.setfElementId(10L);
element.setfElementName("222");
mapper.insertSelective(element);
});
return "OK";
}
});
log.info("end...");
return "ok";
}
3.結論
示例1
element.setfElementId(10L); 為主鍵。SQL在第一次插入id=10的時候是沒有問題的,在第二次插入id=10的時候,由於主鍵衝突了,導致報錯,然後整個事務都會進行回滾,這是沒有問題的。是spring的事務幫助我們來進行回滾等操作的。我們可以看到如下程式碼,他是對整個result = action.doInTransaction(status);進行了try catch。如果拋異常,就會回滾
@Override
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
示例2
示例2首先是transactionTemplate.execute是一個主main執行緒。然後在第一個子執行緒插入了一個資料,第二個子執行緒也插入了一個資料。那麼現在就是有三個執行緒,一個是main執行緒,一個是A執行緒,一個是B執行緒。
main執行緒正常執行不報錯,A執行緒正常插入不報錯,B執行緒由於主鍵衝突報錯。
我們可以透過上面action.doInTransaction(status);看出來,他對這塊程式碼進行了try catch。也就是主執行緒進行了try catch。那麼也就是隻要主執行緒沒有報錯,這個事務就不會被捕獲,也就不會回滾了。無論你A,B還是CDEFG子執行緒出問題了,只要不影響main執行緒,那事務就不會回滾呢?
因此我們可以得出一個結論,在示例2中,A執行緒會插入成功,B執行緒插入失敗,事務不會回滾,最終插入成功。這個其實與我們平常的想法所違背了。
因此如果想要主執行緒丟擲異常,得讓主執行緒感知到子執行緒異常了,主動地去throw異常。比如我們可以設定一個flag,子執行緒報錯了 flag=true。主執行緒檢測到flag為true,就主動丟擲一個exception
4.最後
這道面試題非常有意思,起初以為會回滾,沒想到不會回滾。檢視程式碼得知,原來是catch住的是主執行緒,並不是子執行緒。同樣註解式事務類似。因此如果想要事務生效,儘量避免在事務中使用多執行緒來進行插入更新等操作