【Spring】@Transactional 閒聊
阿新 • • 發佈:2020-06-25
菜瓜:上次的AOP理論知識看完收穫挺多的,雖然有一個自定義註解的demo,但還是覺得差點東西
水稻:我也覺得沒有跟一遍原始碼還是差點意思,這次結合@Transactional註解深入原始碼看一下
菜瓜:事務註解,這個平時用的挺多的
水稻:是嗎?來看看你的基礎咋樣
- 要保證一個方法中多個資料庫操作的原子性,要共用一個資料庫連線,但是coding時我們不用顯示傳遞連線物件,這是咋弄的?
- 如果一個方法裡面只有查詢操作,是否不用開啟事務?
- 如何解決非事務方法呼叫本地事務方法失效的?
- 註解常用的傳播屬性,你知道他們的區別嗎
菜瓜:雖然沒看過原始碼,我大膽猜測一下
- 隱式傳遞連線物件可以將其封裝到執行緒中,一般一次請求操作都是在一個執行緒中完成。使用ThreadLocal將連線和執行緒繫結
- 查詢操作也得看業務場景,如果多次查詢相同的資料要避免不可重複讀問題,可開啟只讀事務 (readOnly = true)
- 結合AOP的知識,這裡其實要解決呼叫事務方法的物件不是代理物件的問題。用代理物件調本地事務方法即可(注入自己)
-
/**
* @author QuCheng on 2020/6/24.
*/
@Service
public class ItemServiceImpl implements ItemService { @Resource
private IcbcItemMapper itemMapper; @Resource
private ItemService itemService; @Override
// changeItemById(itemId);
itemService.changeItemById(itemId);
} @Transactional(rollbackFor = RuntimeException.class)
@Override
public void changeItemById(Long itemId) {
itemMapper.updateNameById(itemId, "name4");
int a = 10 / 0;
}
}
-
- 傳播屬性這個沒瞭解過啊,資料庫事務裡面麼得這個概念
水稻:可以啊,平時的程式碼沒白寫
菜瓜:coding這種事情,easy啦!
水稻:這就飄了?來看這個問題
- 如果我想在A事務方法中呼叫B事務方法,B方法如果回滾了,不能影響A事務繼續執行,但是A事務如果執行出問題了,B也要回滾,怎麼弄?
菜瓜:。。。這不就是大事務巢狀小事務嘛。。。我不會
水稻:不扯了,來看原始碼吧,這個問題等解釋了傳播屬性你就知道了
- 上回我們說到,@Transactional是AOP的典型應用,bean被例項化之後要建立代理(參考自定義註解),就少不了切面類Advisor物件。那麼它是誰,它在哪,它在幹什麼?
- 回到夢開始的地方,事務功能開啟的註解@EnableTransactionManagement
- 沒錯,它肯定會有一個Import註解引入TransactionManagementConfigurationSelector類,它又引入了切面類
-
public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> { @Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
// 看這裡
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}
。。。 } @Configuration
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(transactionAttributeSource());
advisor.setAdvice(transactionInterceptor());
if (this.enableTx != null) {
advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
}
return advisor;
} @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource();
} @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionInterceptor transactionInterceptor() {
// 增強
TransactionInterceptor interceptor = new TransactionInterceptor();
interceptor.setTransactionAttributeSource(transactionAttributeSource());
if (this.txManager != null) {
interceptor.setTransactionManager(this.txManager);
}
return interceptor;
} } - 切面類物件設定了事務的掃描器,也set了增強類TransactionInterceptor
-
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
。。。
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
} public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean { 。。。
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
。。。
// ①建立事務,資料庫連線處理也在這
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 呼叫目標方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 異常後事務處理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
。。。
}
菜瓜:懂,接下來的程式碼邏輯就是在增強類TransactionInterceptor的invoke方法裡
水稻:對
- 先看資料庫連線的處理 - 驗證ThreadLocal
-
protected void doBegin(Object transaction, TransactionDefinition definition) {
。。。
// 如果連線是新的,就進行繫結
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
}
。。。
} class TransactionSynchronizationManager
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = resources.get();
// set ThreadLocal Map if none found
if (map == null) {
map = new HashMap<>();
resources.set(map);
}
Object oldValue = map.put(actualKey, value);
// Transparently suppress a ResourceHolder that was marked as void...
if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
oldValue = null;
}
if (oldValue != null) {
throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
}
if (logger.isTraceEnabled()) {
logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
Thread.currentThread().getName() + "]");
}
} - 回過頭來看AB方法呼叫的回滾問題,直接給出答案(突然發現這個問題要講清楚篇幅會很大,就。。挺突然的。。挺突然der)
- 在B方法上設定傳播屬性為NESTED即可,然後在A中catch住B的異常
- 你肯定會問我不加NESTED去catch不行嗎?不行,非NESTED的方法丟擲的異常是無法回滾的。
- 不信你看
-
@Transactional(rollbackFor = RuntimeException.class)
@Override
public void changeNameById(Long itemId) {
itemMapper.updateNameById(itemId, "A");
try {
itemService.changeItemById(itemId);
// itemService.changeItemByIdNested(itemId);
} catch (Exception e) {
System.out.println("我想繼續執行,不影響修改A");
}
itemMapper.updatePriceById(itemId, 1L);
} @Transactional(rollbackFor = RuntimeException.class)
@Override
public void changeItemById(Long itemId) {
itemMapper.updateNameById(itemId, "B+REQUIRED");
itemMapper.updatePriceById(itemId, 10L);
int a = 10 / 0;
} @Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.NESTED)
@Override
public void changeItemByIdNested(Long itemId) {
itemMapper.updateNameById(itemId, "B+NESTED");
itemMapper.updatePriceById(itemId, 100L);
int a = 10 / 0;
} -- 測試結果
//① itemService.changeItemById(itemId); 資料庫所有資料都不會改變
我想繼續執行,不影響修改A
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only // ② itemService.changeItemByIdNested(itemId); 第一個方法的修改會生效
我想繼續執行,不影響修改A
菜瓜:這就是傳播屬性NESTED?預設的是REQUIRED,還有一個常用的REQUIRES_NEW呢?
水稻:搞清楚這個其實從資料庫連線入手其實就很清楚
- REQUIRED修飾的方法和A使用同一個連線,A和B是掛一起的,誰回滾都會影響對方,且B方法的異常會被事務管理器標記為必須回滾
- NESTED修飾的方法和A使用同一個連線,但是用到了資料庫的savePoint特性,它可以回滾到指定的點,如果是有回滾點的操作,丟擲的異常可以被處理
- REQUIRES_NEW修飾的方法和A使用的就不是一個連線了,回不回滾都不會影響對方,當然,要捕捉異常
菜瓜:傳播屬性瞭解。回滾的問題還得再看看,篇幅很大是很複雜嗎?
水稻:其實不復雜,就是要跟蹤原始碼斷點除錯。。。截圖搞來搞去,篇幅就很長,你自己去調的話其實很快
菜瓜:那我下去康康
總結
- 這裡提到Transactional註解其實是為了鞏固AOP的,當然提到了一些注意點。譬如本地呼叫,譬如ThreadLocal的應用,還譬如傳播屬性
- 傳播屬性其實用的少,但是聊起來就比較多了,可以聊事務的隔離級別,聊ACID的實現,聊MySQL的鎖