Spring聲明式事務在哪些情況下會失效?
編程式事務
在Spring中事務管理的方式有兩種,編程式事務和聲明式事務。先詳細介紹一下兩種事務的實現(xiàn)方式.
配置類
- @Configuration
- @EnableTransactionManagement
- @ComponentScan("com.javashitang")
- public class AppConfig {
- @Bean
- public DruidDataSource dataSource() {
- DruidDataSource ds = new DruidDataSource();
- ds.setDriverClassName("com.mysql.jdbc.Driver");
- ds.setUrl("jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true");
- ds.setUsername("test");
- ds.setPassword("test");
- ds.setInitialSize(5);
- return ds;
- }
- @Bean
- public DataSourceTransactionManager dataSourceTransactionManager() {
- return new DataSourceTransactionManager(dataSource());
- }
- @Bean
- public JdbcTemplate jdbcTemplate(DataSource dataSource) {
- return new JdbcTemplate(dataSource);
- }
- @Bean
- public TransactionTemplate transactionTemplate() {
- return new TransactionTemplate(dataSourceTransactionManager());
- }
- }
- public interface UserService {
- void addUser(String name, String location);
- default void doAdd(String name) {};
- }
- @Service
- public class UserServiceV1Impl implements UserService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Autowired
- private TransactionTemplate transactionTemplate;
- @Override
- public void addUser(String name, String location) {
- transactionTemplate.execute(new TransactionCallbackWithoutResult() {
- @Override
- protected void doInTransactionWithoutResult(TransactionStatus status) {
- try {
- String sql = "insert into user (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{name});
- throw new RuntimeException("保存用戶信息失敗");
- } catch (Exception e) {
- e.printStackTrace();
- status.setRollbackOnly();
- }
- }
- });
- }
- }
可以看到編程式事務的方式并不優(yōu)雅,因為業(yè)務代碼和事務代碼耦合到一塊,當發(fā)生異常的時候還得需要手動回滾事務(比使用JDBC方便多類,JDBC得先關閉自動自動提交,然后根據(jù)情況手動提交或者回滾事務)
如果讓你優(yōu)化事務方法的執(zhí)行?你會如何做?
「其實我們完全可以用AOP來優(yōu)化這種代碼,設置好切點,當方法執(zhí)行成功時提交事務,當方法發(fā)生異常時回滾事務,這就是聲明式事務的實現(xiàn)原理」
使用AOP后,當我們調(diào)用事務方法時,會調(diào)用到生成的代理對象,代理對象中加入了事務提交和回滾的邏輯。
聲明式事務
Spring aop動態(tài)代理的方式有如下幾種方法
JDK動態(tài)代理實現(xiàn)(基于接口)(JdkDynamicAopProxy)
CGLIB動態(tài)代理實現(xiàn)(動態(tài)生成子類的方式)(CglibAopProxy)
AspectJ適配實現(xiàn)
spring aop默認只會使用JDK和CGLIB來生成代理對象
@Transactional可以用在哪里?
@Transactional可以用在類,方法,接口上
用在類上,該類的所有public方法都具有事務
用在方法上,方法具有事務。當類和方法同時配置事務的時候,方法的屬性會覆蓋類的屬性
用在接口上,一般不建議這樣使用,因為只有基于接口的代理會生效,如果Spring AOP使用cglib來實現(xiàn)動態(tài)代理,會導致事務失效(因為注解不能被繼承)
@Transactional失效的場景
@Transactional注解應用到非public方法(除非特殊配置,例如使用AspectJ 靜態(tài)織入實現(xiàn) AOP)
自調(diào)用,因為@Transactional是基于動態(tài)代理實現(xiàn)的
異常在代碼中被你自己try catch了
異常類型不正確,默認只支持RuntimeException和Error,不支持檢查異常
事務傳播配置不符合業(yè)務邏輯
@Transactional注解應用到非public方法
「為什么只有public方法上的@Transactional注解才會生效?」
首相JDK動態(tài)代理肯定只能是public,因為接口的權限修飾符只能是public。cglib代理的方式是可以代理protected方法的(private不行哈,子類訪問不了父類的private方法)如果支持protected,可能會造成當切換代理的實現(xiàn)方式時表現(xiàn)不同,增大出現(xiàn)bug的可能醒,所以統(tǒng)一一下。
「如果想讓非public方法也生效,你可以考慮使用AspectJ」
自調(diào)用,因為@Transactional是基于動態(tài)代理實現(xiàn)的
當自調(diào)用時,方法執(zhí)行不會經(jīng)過代理對象,所以會導致事務失效。例如通過如下方式調(diào)用addUser方法時,事務會失效
- // 事務失效
- @Service
- public class UserServiceV2Impl implements UserService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Override
- public void addUser(String name, String location) {
- doAdd(name);
- }
- @Transactional
- public void doAdd(String name) {
- String sql = "insert into user (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{name});
- throw new RuntimeException("保存用戶失敗");
- }
- }
可以通過如下方式解決
- @Autowired注入自己,假如為self,然后通過self調(diào)用方法
- @Autowired ApplicationContext,從ApplicationContext通過getBean獲取自己,然后再調(diào)用
- // 事務生效
- @Service
- public class UserServiceV2Impl implements UserService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Autowired
- private UserService userService;
- @Override
- public void addUser(String name, String location) {
- userService.doAdd(name);
- }
- @Override
- @Transactional
- public void doAdd(String name) {
- String sql = "insert into user (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{name});
- throw new RuntimeException("保存用戶失敗");
- }
- }
異常在代碼中被你自己try catch了
這個邏輯從源碼理解比較清晰,只有當執(zhí)行事務拋出異常才能進入completeTransactionAfterThrowing方法,這個方法里面有回滾的邏輯,如果事務方法都沒拋出異常就只會正常提交
- // org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
- try {
- // This is an around advice: Invoke the next interceptor in the chain.
- // This will normally result in a target object being invoked.
- // 執(zhí)行事務方法
- retVal = invocation.proceedWithInvocation();
- }
- catch (Throwable ex) {
- // target invocation exception
- completeTransactionAfterThrowing(txInfo, ex);
- throw ex;
- }
- finally {
- cleanupTransactionInfo(txInfo);
- }
異常類型不正確,默認只支持RuntimeException和Error,不支持檢查異常
異常體系圖如下。當拋出檢查異常時,spring事務不會回滾。如果拋出任何異常都回滾,可以配置rollbackFor為Exception
- @Transactional(rollbackFor = Exception.class)
事務傳播配置不符合業(yè)務邏輯
假如說有這樣一個場景,用戶注冊,依次保存用戶基本信息到user表中,用戶住址信息到地址表中,當保存用戶住址信息失敗時,我們也要保證用戶信息注冊成功。
- public interface LocationService {
- void addLocation(String location);
- }
- @Service
- public class LocationServiceImpl implements LocationService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Override
- @Transactional
- public void addLocation(String location) {
- String sql = "insert into location (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{location});
- throw new RuntimeException("保存地址異常");
- }
- }
- @Service
- public class UserServiceV3Impl implements UserService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Autowired
- private LocationService locationService;
- @Override
- @Transactional
- public void addUser(String name, String location) {
- String sql = "insert into user (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{name});
- locationService.addLocation(location);
- }
- }
調(diào)用發(fā)現(xiàn)user表和location表都沒有插入數(shù)據(jù),并不符合我們期望,你可能會說拋出異常了,事務當然回滾了。好,我們把調(diào)用locationService的部分加上try catch
- @Service
- public class UserServiceV3Impl implements UserService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Autowired
- private LocationService locationService;
- @Override
- @Transactional
- public void addUser(String name, String location) {
- String sql = "insert into user (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{name});
- try {
- locationService.addLocation(location);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
調(diào)用發(fā)現(xiàn)user表和location表還是都沒有插入數(shù)據(jù)。這是因為在LocationServiceImpl中事務已經(jīng)被標記成回滾了,所以最終事務還會回滾。
要想最終解決就不得不提到Spring的事務傳播行為了,不清楚的小伙伴看《面試官:Spring事務的傳播行為有幾種?》
Transactional的事務傳播行為默認為Propagation.REQUIRED?!溉绻斍按嬖谑聞?,則加入該事務。如果當前沒有事務,則創(chuàng)建一個新的事務」
此時我們把LocationServiceImpl中Transactional的事務傳播行為改成Propagation.REQUIRES_NEW即可
「創(chuàng)建一個新事務,如果當前存在事務,則把當前事務掛起」
所以最終的解決代碼如下
- @Service
- public class UserServiceV3Impl implements UserService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Autowired
- private LocationService locationService;
- @Override
- @Transactional
- public void addUser(String name, String location) {
- String sql = "insert into user (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{name});
- try {
- locationService.addLocation(location);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- @Service
- public class LocationServiceImpl implements LocationService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Override
- @Transactional(propagation = Propagation.REQUIRES_NEW)
- public void addLocation(String location) {
- String sql = "insert into location (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{location});
- throw new RuntimeException("保存地址異常");
- }
- }
- @Service
- public class LocationServiceImpl implements LocationService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Override
- @Transactional(propagation = Propagation.REQUIRES_NEW)
- public void addLocation(String location) {
- String sql = "insert into location (`name`) values (?)";
- jdbcTemplate.update(sql, new Object[]{location});
- throw new RuntimeException("保存地址異常");
- }
- }