javascript
每日一博 - 常见的Spring事务失效事务不回滚案例集锦
文章目錄
- 事務(wù)不生效
- 方法內(nèi)部調(diào)用
- 修復(fù)方法一 : 【新加一個(gè)Service方法】
- 修復(fù)方法二:【在該Service類(lèi)中注入自己】
- 修復(fù)方法三:【通過(guò)AopContent類(lèi)】<---- 推薦
- 訪(fǎng)問(wèn)權(quán)限問(wèn)題
- 方法用final修飾
- 未被spring管理
- 多線(xiàn)程調(diào)用
- 表不支持事務(wù)
- 未開(kāi)啟事務(wù)
- 事務(wù)不回滾
- 錯(cuò)誤的傳播特性
- 自己吞了異常
- 手動(dòng)拋了別的異常
- 自定義了回滾異常
- 嵌套事務(wù)回滾多了
- 其他常見(jiàn)問(wèn)題
- 編程式事務(wù)
- 大事務(wù)問(wèn)題
- 小結(jié)
事務(wù)不生效
方法內(nèi)部調(diào)用
有時(shí)候我們需要在某個(gè)Service類(lèi)的某個(gè)方法中,調(diào)用另外一個(gè)事務(wù)方法
@Service public class UserService {@Autowiredprivate UserMapper userMapper;@Transactionalpublic void add(UserModel userModel) {userMapper.insertUser(userModel);updateStatus(userModel);}@Transactionalpublic void updateStatus(UserModel userModel) {doSameThing();} }在事務(wù)方法add中,直接調(diào)用事務(wù)方法updateStatus。 updateStatus方法擁有事務(wù)的能力是因?yàn)閟pring aop生成代理了對(duì)象,但是這種方法直接調(diào)用了this對(duì)象的方法,所以u(píng)pdateStatus方法不會(huì)生成事務(wù)。
由此可見(jiàn),在同一個(gè)類(lèi)中的方法直接內(nèi)部調(diào)用,會(huì)導(dǎo)致事務(wù)失效。
那么問(wèn)題來(lái)了,如果有些場(chǎng)景,確實(shí)想在同一個(gè)類(lèi)的某個(gè)方法中,調(diào)用它自己的另外一個(gè)方法,該怎么辦呢?
修復(fù)方法一 : 【新加一個(gè)Service方法】
只需要新加一個(gè)Service方法,把@Transactional注解加到新Service方法上,把需要事務(wù)執(zhí)行的代碼移到新方法中。
具體代碼如下:
@Servcie public class ServiceA {@Autowiredprvate ServiceB serviceB;public void save(User user) {queryData1();queryData2();serviceB.doSave(user);}}@Servciepublic class ServiceB {@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}修復(fù)方法二:【在該Service類(lèi)中注入自己】
如果不想再新加一個(gè)Service類(lèi),在該Service類(lèi)中注入自己也是一種選擇。
具體代碼如下:
@Servcie public class ServiceA {@Autowiredprvate ServiceA serviceA;public void save(User user) {queryData1();queryData2();serviceA.doSave(user);}@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}注入自己 spring ioc內(nèi)部的三級(jí)緩存保證了它,不會(huì)出現(xiàn)循環(huán)依賴(lài)問(wèn)題。
修復(fù)方法三:【通過(guò)AopContent類(lèi)】<---- 推薦
在該Service類(lèi)中使用AopContext.currentProxy()獲取代理對(duì)象
上面的方法2確實(shí)可以解決問(wèn)題,但是代碼看起來(lái)并不直觀(guān),還可以通過(guò)在該Service類(lèi)中使用AOPProxy獲取代理對(duì)象,實(shí)現(xiàn)相同的功能。具體代碼如下:
@Servcie public class ServiceA {public void save(User user) {queryData1();queryData2();((ServiceA)AopContext.currentProxy()).doSave(user);}@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}訪(fǎng)問(wèn)權(quán)限問(wèn)題
@Service public class UserService {@Transactionalprivate void add(UserModel userModel) {saveData(userModel);updateData(userModel);} }add方法的訪(fǎng)問(wèn)權(quán)限被定義成了private,這樣會(huì)導(dǎo)致事務(wù)失效,spring要求被代理方法必須是public的。
源碼
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute方法中有個(gè)判斷,如果目標(biāo)方法不是public,則TransactionAttribute返回null,即不支持事務(wù)。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {// Don't allow no-public methods as required.if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {return null;}............}如果我們自定義的事務(wù)方法(即目標(biāo)方法),它的訪(fǎng)問(wèn)權(quán)限不是public,而是private、default或protected的話(huà),spring則不會(huì)提供事務(wù)功能。
方法用final修飾
有時(shí)候,某個(gè)方法不想被子類(lèi)重新,這時(shí)可以將該方法定義成final的。普通方法這樣定義是沒(méi)問(wèn)題的,但如果將事務(wù)方法定義成final,例如:
@Service public class UserService {@Transactionalpublic final void add(UserModel userModel){saveData(userModel);updateData(userModel);} }我們可以看到add方法被定義成了final的,這樣會(huì)導(dǎo)致事務(wù)失效。
為什么?
spring事務(wù)底層使用了aop,也就是通過(guò)jdk動(dòng)態(tài)代理或者cglib,幫我們生成了代理類(lèi),在代理類(lèi)中實(shí)現(xiàn)的事務(wù)功能。
但如果某個(gè)方法用final修飾了,那么在它的代理類(lèi)中,就無(wú)法重寫(xiě)該方法,而添加事務(wù)功能。
注意:如果某個(gè)方法是static的,同樣無(wú)法通過(guò)動(dòng)態(tài)代理,變成事務(wù)方法。
未被spring管理
使用spring事務(wù)的前提是:對(duì)象要被spring管理,需要?jiǎng)?chuàng)建bean實(shí)例。
通常情況下,我們通過(guò)@Controller、@Service、@Component、@Repository等注解,可以自動(dòng)實(shí)現(xiàn)bean實(shí)例化和依賴(lài)注入的功能。
如果忘了加@Service注解,比如:
public class UserService {@Transactionalpublic void add(UserModel userModel) {saveData(userModel);updateData(userModel);} }該類(lèi)不會(huì)交給spring管理,所以它的add方法也不會(huì)生成事務(wù)。
多線(xiàn)程調(diào)用
@Slf4j @Service public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {userMapper.insertUser(userModel);new Thread(() -> {roleService.doOtherThing();}).start();} }@Service public class RoleService {@Transactionalpublic void doOtherThing() {System.out.println("保存role表數(shù)據(jù)");} }事務(wù)方法add中,調(diào)用了事務(wù)方法doOtherThing,但是事務(wù)方法doOtherThing是在另外一個(gè)線(xiàn)程中調(diào)用的。
這樣會(huì)導(dǎo)致兩個(gè)方法不在同一個(gè)線(xiàn)程中,獲取到的數(shù)據(jù)庫(kù)連接不一樣,從而是兩個(gè)不同的事務(wù)。如果想doOtherThing方法中拋了異常,add方法也回滾是不可能的。
spring的事務(wù)是通過(guò)數(shù)據(jù)庫(kù)連接來(lái)實(shí)現(xiàn)的。當(dāng)前線(xiàn)程中保存了一個(gè)map,key是數(shù)據(jù)源,value是數(shù)據(jù)庫(kù)連接。
private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");我們說(shuō)的同一個(gè)事務(wù),其實(shí)是指同一個(gè)數(shù)據(jù)庫(kù)連接,只有擁有同一個(gè)數(shù)據(jù)庫(kù)連接才能同時(shí)提交和回滾。如果在不同的線(xiàn)程,拿到的數(shù)據(jù)庫(kù)連接肯定是不一樣的,所以是不同的事務(wù)。
表不支持事務(wù)
在mysql5之前,默認(rèn)的數(shù)據(jù)庫(kù)引擎是myisam。 索引文件和數(shù)據(jù)文件是分開(kāi)存儲(chǔ)的,對(duì)于查多寫(xiě)少的單表操作,性能比innodb更好。
myisam不支持事務(wù)。myisam還不支持行鎖和外鍵。
建議我們?cè)陂_(kāi)發(fā)的過(guò)程中,發(fā)現(xiàn)某張表的事務(wù)一直都沒(méi)有生效,那不一定是spring事務(wù)的鍋,最好確認(rèn)一下你使用的那張表,是否支持事務(wù)。
未開(kāi)啟事務(wù)
有時(shí)候,事務(wù)沒(méi)有生效的根本原因是沒(méi)有開(kāi)啟事務(wù)。
如果你使用的是springboot項(xiàng)目,那么你很幸運(yùn)。因?yàn)閟pringboot通過(guò)DataSourceTransactionManagerAutoConfiguration類(lèi),已經(jīng)默默的幫你開(kāi)啟了事務(wù)。
你所要做的事情很簡(jiǎn)單,只需要配置spring.datasource相關(guān)參數(shù)即可。
但如果你使用的還是傳統(tǒng)的spring項(xiàng)目,則需要在applicationContext.xml文件中,手動(dòng)配置事務(wù)相關(guān)參數(shù)。如果忘了配置,事務(wù)肯定是不會(huì)生效的。
<!-- 配置事務(wù)管理器 --> <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <tx:advice id="advice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/></tx:attributes> </tx:advice> <!-- 用切點(diǎn)把事務(wù)切進(jìn)去 --> <aop:config> <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> </aop:config>默默的說(shuō)一句,如果在pointcut標(biāo)簽中的切入點(diǎn)匹配規(guī)則,配錯(cuò)了的話(huà),有些類(lèi)的事務(wù)也不會(huì)生效。
事務(wù)不回滾
錯(cuò)誤的傳播特性
在使用@Transactional注解時(shí),是可以指定propagation參數(shù)的。
該參數(shù)的作用是指定事務(wù)的傳播特性,spring目前支持7種傳播特性:
- REQUIRED 如果當(dāng)前上下文中存在事務(wù),那么加入該事務(wù),如果不存在事務(wù),創(chuàng)建一個(gè)事務(wù),這是默認(rèn)的傳播屬性值。
- SUPPORTS 如果當(dāng)前上下文存在事務(wù),則支持事務(wù)加入事務(wù),如果不存在事務(wù),則使用非事務(wù)的方式執(zhí)行。
- MANDATORY 如果當(dāng)前上下文中存在事務(wù),否則拋出異常。
- REQUIRES_NEW 每次都會(huì)新建一個(gè)事務(wù),并且同時(shí)將上下文中的事務(wù)掛起,執(zhí)行當(dāng)前新建事務(wù)完成以后,上下文事務(wù)恢復(fù)再執(zhí)行。
- NOT_SUPPORTED 如果當(dāng)前上下文中存在事務(wù),則掛起當(dāng)前事務(wù),然后新的方法在沒(méi)有事務(wù)的環(huán)境中執(zhí)行。
- NEVER 如果當(dāng)前上下文中存在事務(wù),則拋出異常,否則在無(wú)事務(wù)環(huán)境上執(zhí)行代碼。
- NESTED 如果當(dāng)前上下文中存在事務(wù),則嵌套事務(wù)執(zhí)行,如果不存在事務(wù),則新建事務(wù)。
如果我們?cè)谑謩?dòng)設(shè)置propagation參數(shù)的時(shí)候,把傳播特性設(shè)置錯(cuò)了,比如:
@Service public class UserService {@Transactional(propagation = Propagation.NEVER)public void add(UserModel userModel) {saveData(userModel);updateData(userModel);} }add方法的事務(wù)傳播特性定義成了Propagation.NEVER,這種類(lèi)型的傳播特性不支持事務(wù),如果有事務(wù)則會(huì)拋異常。
目前只有這三種傳播特性才會(huì)創(chuàng)建新事務(wù):REQUIRED,REQUIRES_NEW,NESTED。
自己吞了異常
事務(wù)不會(huì)回滾,最常見(jiàn)的問(wèn)題是:開(kāi)發(fā)者在代碼中手動(dòng)try…catch了異常。
@Slf4j @Service public class UserService {@Transactionalpublic void add(UserModel userModel) {try {saveData(userModel);updateData(userModel);} catch (Exception e) {log.error(e.getMessage(), e);}} }這種情況下spring事務(wù)當(dāng)然不會(huì)回滾,因?yàn)殚_(kāi)發(fā)者自己捕獲了異常,又沒(méi)有手動(dòng)拋出,換句話(huà)說(shuō)就是把異常吞掉了。
如果想要spring事務(wù)能夠正?;貪L,必須拋出它能夠處理的異常。如果沒(méi)有拋異常,則spring認(rèn)為程序是正常的。
手動(dòng)拋了別的異常
即使開(kāi)發(fā)者沒(méi)有手動(dòng)捕獲異常,但如果拋的異常不正確,spring事務(wù)也不會(huì)回滾。
@Slf4j @Service public class UserService {@Transactionalpublic void add(UserModel userModel) throws Exception {try {saveData(userModel);updateData(userModel);} catch (Exception e) {log.error(e.getMessage(), e);throw new Exception(e);}} }上面的這種情況,開(kāi)發(fā)人員自己捕獲了異常,又手動(dòng)拋出了異常:Exception,事務(wù)同樣不會(huì)回滾。
因?yàn)閟pring事務(wù),默認(rèn)情況下只會(huì)回滾RuntimeException(運(yùn)行時(shí)異常)和Error(錯(cuò)誤),對(duì)于普通的Exception(非運(yùn)行時(shí)異常),它不會(huì)回滾。
自定義了回滾異常
在使用@Transactional注解聲明事務(wù)時(shí),有時(shí)我們想自定義回滾的異常,spring也是支持的??梢酝ㄟ^(guò)設(shè)置rollbackFor參數(shù),來(lái)完成這個(gè)功能。
但如果這個(gè)參數(shù)的值設(shè)置錯(cuò)了,就會(huì)引出一些莫名其妙的問(wèn)題,例如:
@Slf4j @Service public class UserService {@Transactional(rollbackFor = BusinessException.class)public void add(UserModel userModel) throws Exception {saveData(userModel);updateData(userModel);} }如果在執(zhí)行上面這段代碼,保存和更新數(shù)據(jù)時(shí),程序報(bào)錯(cuò)了,拋了SqlException、DuplicateKeyException等異常。而B(niǎo)usinessException是我們自定義的異常,報(bào)錯(cuò)的異常不屬于BusinessException,所以事務(wù)也不會(huì)回滾。
即使rollbackFor有默認(rèn)值,但阿里巴巴開(kāi)發(fā)者規(guī)范中,還是要求開(kāi)發(fā)者重新指定該參數(shù)。
這是為什么呢?
因?yàn)槿绻褂媚J(rèn)值,一旦程序拋出了Exception,事務(wù)不會(huì)回滾,這會(huì)出現(xiàn)很大的bug。所以,建議一般情況下,將該參數(shù)設(shè)置成:Exception或Throwable。
嵌套事務(wù)回滾多了
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {userMapper.insertUser(userModel);roleService.doOtherThing();} }@Service public class RoleService {@Transactional(propagation = Propagation.NESTED)public void doOtherThing() {System.out.println("保存role表數(shù)據(jù)");} }這種情況使用了嵌套的內(nèi)部事務(wù),原本是希望調(diào)用roleService.doOtherThing方法時(shí),如果出現(xiàn)了異常,只回滾doOtherThing方法里的內(nèi)容,不回滾 userMapper.insertUser里的內(nèi)容,即回滾保存點(diǎn)。。但事實(shí)是,insertUser也回滾了。
why?
因?yàn)閐oOtherThing方法出現(xiàn)了異常,沒(méi)有手動(dòng)捕獲,會(huì)繼續(xù)往上拋,到外層add方法的代理方法中捕獲了異常。所以,這種情況是直接回滾了整個(gè)事務(wù),不只回滾單個(gè)保存點(diǎn)。
怎么樣才能只回滾保存點(diǎn)呢?
@Slf4j @Service public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {userMapper.insertUser(userModel);try {roleService.doOtherThing();} catch (Exception e) {log.error(e.getMessage(), e);}} }可以將內(nèi)部嵌套事務(wù)放在try/catch中,并且不繼續(xù)往上拋異常。這樣就能保證,如果內(nèi)部嵌套事務(wù)中出現(xiàn)異常,只回滾內(nèi)部事務(wù),而不影響外部事務(wù)。
其他常見(jiàn)問(wèn)題
編程式事務(wù)
上面聊的這些內(nèi)容都是基于@Transactional注解的,主要說(shuō)的是它的事務(wù)問(wèn)題,我們把這種事務(wù)叫做:聲明式事務(wù)。
其實(shí),spring還提供了另外一種創(chuàng)建事務(wù)的方式,即通過(guò)手動(dòng)編寫(xiě)代碼實(shí)現(xiàn)的事務(wù),我們把這種事務(wù)叫做:編程式事務(wù)。例如:
@Autowiredprivate TransactionTemplate transactionTemplate;...public void save(final User user) {queryData1();queryData2();transactionTemplate.execute((status) => {addData1();updateData2();return Boolean.TRUE;})}在spring中為了支持編程式事務(wù),專(zhuān)門(mén)提供了一個(gè)類(lèi):TransactionTemplate,在它的execute方法中,就實(shí)現(xiàn)了事務(wù)的功能。
相較于@Transactional注解聲明式事務(wù),我更建議大家使用,基于TransactionTemplate的編程式事務(wù)。主要原因如下:
- 避免由于spring aop問(wèn)題,導(dǎo)致事務(wù)失效的問(wèn)題。
- 能夠更小粒度的控制事務(wù)的范圍,更直觀(guān)。
大事務(wù)問(wèn)題
在使用spring事務(wù)時(shí),有個(gè)讓人非常頭疼的問(wèn)題,就是大事務(wù)問(wèn)題。
通常情況下,我們會(huì)在方法上@Transactional注解,填加事務(wù)功能,比如:
@Service public class UserService {@Autowired private RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {query1();query2();query3();roleService.save(userModel);update(userModel);} }@Service public class RoleService {@Autowired private RoleService roleService;@Transactionalpublic void save(UserModel userModel) throws Exception {query4();query5();query6();saveData(userModel);} }但@Transactional注解,如果被加到方法上,有個(gè)缺點(diǎn)就是整個(gè)方法都包含在事務(wù)當(dāng)中了。
上面的這個(gè)例子中,在UserService類(lèi)中,其實(shí)只有這兩行才需要事務(wù):
roleService.save(userModel); update(userModel);在RoleService類(lèi)中,只有這一行需要事務(wù):
saveData(userModel);現(xiàn)在的這種寫(xiě)法,會(huì)導(dǎo)致所有的query方法也被包含在同一個(gè)事務(wù)當(dāng)中。
如果query方法非常多,調(diào)用層級(jí)很深,而且有部分查詢(xún)方法比較耗時(shí)的話(huà),會(huì)造成整個(gè)事務(wù)非常耗時(shí),而從造成大事務(wù)問(wèn)題。
- 死鎖
- 回滾時(shí)間長(zhǎng)
- 并發(fā)情況下,連接池被占滿(mǎn)
- 鎖等待
- 接口超時(shí)
- 主從延遲
小結(jié)
建議在項(xiàng)目中少使用@Transactional注解開(kāi)啟事務(wù)。但并不是說(shuō)一定不能用它,如果項(xiàng)目中有些業(yè)務(wù)邏輯比較簡(jiǎn)單,而且不經(jīng)常變動(dòng),使用@Transactional注解開(kāi)啟事務(wù)開(kāi)啟事務(wù)也無(wú)妨,因?yàn)樗?jiǎn)單,開(kāi)發(fā)效率更高,但是千萬(wàn)要小心事務(wù)失效的問(wèn)題。
總結(jié)
以上是生活随笔為你收集整理的每日一博 - 常见的Spring事务失效事务不回滚案例集锦的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 每日一博 - 延时任务的多种实现方式解读
- 下一篇: Spring5 - 核心原理