首頁 > 軟體

Spring詳細講解事務失效的場景

2022-07-12 14:00:41

1)未被Spring管理

使用Spring事務的前提是:物件要被Spring管理,事務方法所在的類要被載入為bean物件

如果事務方法所在的類沒有被載入為一個bean,那麼事務自然就失效了,範例:

//@Service
public class UserServiceImpl {
    @Transactional
    public void doTest() {
        // 業務程式碼
    }
}

2)資料庫引擎不支援事務

以MySQL為例,InnoDB引擎是支援事務的,而像MyISAMMEMORY等是不支援事務的。

從MySQL5.5.5開始預設的儲存引擎是InnoDB,之前預設都是MyISAM。所以在開發過程中發現事務失效,不一定是Spring的鍋,最好確認一下資料庫表是否支援事務。

3)事務方法沒有被public修飾

眾所周知,java的存取許可權修飾符有:privatedefaultprotectedpublic四種,

但是@Transactional註解只能作用於public修飾的方法上,

AbstractFallbackTransactionAttributeSource類(Spring通過這個類獲取@Transactional註解的設定屬性資訊)的computeTransactionAttribute方法中有個判斷,如果目標方法不是public,則TransactionAttribute返回null,即不支援事務。

@Nullable
	protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
		// Don't allow no-public methods as required.
		if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
			return null;
		}
        //………………
	}

其實想想動態代理的原理就很好理解了,動態代理是通過實現介面或者繼承來實現的,所以目標方法必須是public修飾,並且不能是final修飾。

4)方法使用final修飾

如果一個方法不想被子類重寫,那麼我們就可以把他寫成final修飾的方法

如果事務方法使用final修飾,那麼aop就無法在代理類中重寫該方法,事務就不會生效

同樣的,static修飾的方法也無法通過代理變成事務方法

5)同一類中方法呼叫

假如在某個Service的方法中,呼叫了另外一個事務方法:

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    public void del(){
        doTest();
    }
    @Transactional
    public void doTest() {
        userMapper.deleteById(200108);
        int i = 10/0; //模擬發生異常
    }
}

像上面的程式碼,doTest方法使用@Transactional註解標註,在del()方法中呼叫了doTest()方法,在外部呼叫del()方法時,事務也不會生效,因為這裡del()方法中呼叫的是類本身的方法,而不是代理物件的方法。

那麼如果確實有這樣的需求怎麼辦呢?

引入自身bean

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Autowired
    UserServiceImpl userServiceImpl;
    public void del(){
        userServiceImpl.doTest();
    }
    @Transactional
    public void doTest() {
        userMapper.deleteById(200112);
        int i = 10/0; //模擬發生異常
    }
}

通過ApplicationContext引入bean

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Autowired
    ApplicationContext applicationContext;
    public void del(){
       ((UserServiceImpl)applicationContext.getBean("userServiceImpl")).doTest();
    }
    @Transactional
    public void doTest() {
        userMapper.deleteById(200112);
        int i = 10/0; //模擬發生異常
    }
}

通過AopContext獲取當前代理類

在啟動類上新增註解@EnableAspectJAutoProxy(exposeProxy = true),表示是否對外暴露代理物件,即是否可以獲取AopContext

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Autowired
    ApplicationContext applicationContext;
    public void del(){
        ((UserServiceImpl)AopContext.currentProxy()).doTest();
    }
    @Transactional
    public void doTest() {
        userMapper.deleteById(200112);
        int i = 10/0; //模擬發生異常
    }
}

6)未開啟事務

如果是SpringBoot專案,那麼SpringBoot通過DataSourceTransactionManagerAutoConfiguration自動設定類幫我們開啟了事務。

如果是傳統的Spring專案,則需要我們自己設定

<!--        設定事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>
<!--        設定事務通知-->
<tx:advice id="Advice" transaction-manager="transactionManager">
  <!--                設定事務屬性,即哪些方法要執行事務-->
  <tx:attributes>
    <tx:method name="insert*" propagation="REQUIRED"/> <!-- 所有insert開頭的方法,以下同理 -->
    <tx:method name="update*" propagation="REQUIRED"/>
    <tx:method name="delete*" propagation="REQUIRED"/>
  </tx:attributes>
</tx:advice>
<!--        設定事務切面-->
<aop:config>
  <aop:pointcut id="AdviceAop" expression="execution(* com.yy.service..*(..))"/> <!--要執行的方法在哪個包,意思為:com.yy.service下的所有包裡面的包含任意引數的所有方法-->
  <aop:advisor advice-ref="Advice" pointcut-ref="AdviceAop"/> <!-- 設定為AdviceAop執行哪個事務通知 -->
</aop:config>

這樣在執行service包下的增刪改操作的方法時,就開啟事務了,或者使用註解的方式

<!--        設定事務管理器-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <property name="dataSource" ref="dataSource"/>
        </bean>
<!--        註解式事務宣告設定-->
        <tx:annotation-driven transaction-manager="transactionManager" />

7)多執行緒呼叫

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Transactional
    public void doTest() throws InterruptedException {
        userMapper.deleteById(200110);
        new Thread(()->{
            userMapper.deleteById(200112);
            int i = 10/0; //模擬發生異常
        }).start();
    }
}

在事務方法doTest中,啟動了一個新的執行緒,並在新的執行緒中發生了異常,這樣doTest是不會回滾的。

因為兩個操作不在一個執行緒中,獲取到的資料庫連線不一樣,從而是兩個不同的事務,所以也不會回滾。

8)錯誤的傳播行為

Spring定義了7種傳播行為,我們可以通propagation屬性來指定傳播行為引數,目前只有REQUIREDREQUIRES_NEWNESTED會建立新的事務,其他的則會以非事務的方式執行或者丟擲異常

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Transactional(propagation = Propagation.NEVER)
    public void doTest() throws InterruptedException {
        userMapper.deleteById(200114);
        int i = 10/0; //模擬發生異常
    }
}

9)自己try…catch…掉了異常

如果沒有異常丟擲,則Spring認為程式是正常的,就不會回滾

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Transactional
    public void doTest() {
        try{
            userMapper.deleteById(200115);
            int i = 10/0; //模擬發生異常
        }catch (Exception e){
            // 異常操作
        }
    }
}

10)手動丟擲了錯誤的異常

Spring預設只會回滾RuntimeExceptionError對於普通的Exception,不會回滾

如果你想觸發其他異常的回滾,需要在註解上設定一下,如:@Transactional(rollbackFor = Exception.class)

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Transactional
    public void doTest() throws Exception {
        try{
            userMapper.deleteById(200116);
            int i = 10/0; //模擬發生異常
        }catch (Exception e){
            // 異常操作
            throw new Exception();
        }
    }
}

11)自定義回滾異常

rollbackFor 用於指定能夠觸發事務回滾的異常型別,可以指定多個異常型別。

預設是在RuntimeException和Error上回滾。

若異常非設定指定的異常類,則事務失效

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Transactional(rollbackFor = NullPointerException.class)
    public void doTest() throws MyException {
        userMapper.deleteById(200118);
        throw new MyException();
    }
}

即使rollbackFor有預設值,但阿里巴巴開發者規範中,還是要求開發者重新指定該引數。

因為如果使用預設值,一旦程式丟擲了Exception,事務不會回滾,這會出現很大的bug。所以,建議一般情況下,將該引數設定成:Exception或Throwable。

12)巢狀事務回滾多了

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Transactional
    public void doTest() {
        userMapper.deleteById(200118);
        ((UserServiceImpl)AopContext.currentProxy()).test02();
    }
    @Transactional(propagation = Propagation.NESTED)
    public void test02(){
        userMapper.deleteById(200119);
        int i = 10 / 0; //模擬發生異常
    }
}

test02()方法出現了異常,沒有手動捕獲,會繼續往上拋,到外層doTest()方法的代理方法中捕獲了異常。所以,這種情況是直接回滾了整個事務,不只回滾單個儲存點。

如果只回滾單個儲存點,可以將內部巢狀事務放在try/catch中,類似於上面的自己try…catch…掉異常,並且不繼續往上拋異常。這樣就能保證,如果內部巢狀事務中出現異常,只回滾內部事務,而不影響外部事務。

@Service
public class UserServiceImpl {
    @Autowired
    UserMapper userMapper;
    @Transactional
    public void doTest() {
        userMapper.deleteById(200118);
        try{
            ((UserServiceImpl)AopContext.currentProxy()).test02();
        }catch (Exception e){
            
        }
    }
    @Transactional(propagation = Propagation.NESTED)
    public void test02(){
        userMapper.deleteById(200119);
        int i = 10 / 0; //模擬發生異常
    }
}

到此這篇關於Spring詳細講解事務失效的場景的文章就介紹到這了,更多相關Spring事務失效內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com