Spring聲明式事務在哪些情況下會失效?

編程式事務

在Spring中事務管理的方式有兩種,編程式事務和聲明式事務。先詳細介紹一下兩種事務的實現方式

配置類

@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();
                }
            }
        });
    }
}

可以看到編程式事務的方式並不優雅,因爲業務代碼和事務代碼耦合到一塊,當發生異常的時候還得需要手動回滾事務(比使用JDBC方便多類,JDBC得先關閉自動自動提交,然後根據情況手動提交或者回滾事務)

如果讓你優化事務方法的執行?你會如何做?

「其實我們完全可以用AOP來優化這種代碼,設置好切點,當方法執行成功時提交事務,當方法發生異常時回滾事務,這就是聲明式事務的實現原理」

使用AOP後,當我們調用事務方法時,會調用到生成的代理對象,代理對象中加入了事務提交和回滾的邏輯。

聲明式事務

Spring aop動態代理的方式有如下幾種方法

  1. JDK動態代理實現(基於接口)(JdkDynamicAopProxy)
  2. CGLIB動態代理實現(動態生成子類的方式)(CglibAopProxy)
  3. AspectJ適配實現

spring aop默認只會使用JDK和CGLIB來生成代理對象

@Transactional可以用在哪裏?

@Transactional可以用在類,方法,接口上

  1. 用在類上,該類的所有public方法都具有事務
  2. 用在方法上,方法具有事務。當類和方法同時配置事務的時候,方法的屬性會覆蓋類的屬性
  3. 用在接口上,一般不建議這樣使用,因爲只有基於接口的代理會生效,如果Spring AOP使用cglib來實現動態代理,會導致事務失效(因爲註解不能被繼承)

@Transactional失效的場景

  1. @Transactional註解應用到非public方法(除非特殊配置,例如使用AspectJ 靜態織入實現 AOP)
  2. 自調用,因爲@Transactional是基於動態代理實現的
  3. 異常在代碼中被你自己try catch了
  4. 異常類型不正確,默認只支持RuntimeException和Error,不支持檢查異常
  5. 事務傳播配置不符合業務邏輯

@Transactional註解應用到非public方法

「爲什麼只有public方法上的@Transactional註解纔會生效?」

首相JDK動態代理肯定只能是public,因爲接口的權限修飾符只能是public。cglib代理的方式是可以代理protected方法的(private不行哈,子類訪問不了父類的private方法)如果支持protected,可能會造成當切換代理的實現方式時表現不同,增大出現bug的可能醒,所以統一一下。

「如果想讓非public方法也生效,你可以考慮使用AspectJ」

自調用,因爲@Transactional是基於動態代理實現的

當自調用時,方法執行不會經過代理對象,所以會導致事務失效。例如通過如下方式調用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("保存用戶失敗");
    }
}

可以通過如下方式解決

  1. @Autowired注入自己,假如爲self,然後通過self調用方法
  2. @Autowired ApplicationContext,從ApplicationContext通過getBean獲取自己,然後再調用
// 事務生效
@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了

這個邏輯從源碼理解比較清晰,只有當執行事務拋出異常才能進入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.
  // 執行事務方法
  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)

事務傳播配置不符合業務邏輯

假如說有這樣一個場景,用戶註冊,依次保存用戶基本信息到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);
    }
}

調用發現user表和location表都沒有插入數據,並不符合我們期望,你可能會說拋出異常了,事務當然回滾了。好,我們把調用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();
        }
    }
}

調用發現user表和location表還是都沒有插入數據。這是因爲在LocationServiceImpl中事務已經被標記成回滾了,所以最終事務還會回滾。

要想最終解決就不得不提到Spring的事務傳播行爲了,不清楚的小夥伴看《面試官:Spring事務的傳播行爲有幾種?》

Transactional的事務傳播行爲默認爲Propagation.REQUIRED。「如果當前存在事務,則加入該事務。如果當前沒有事務,則創建一個新的事務」

此時我們把LocationServiceImpl中Transactional的事務傳播行爲改成Propagation.REQUIRES_NEW即可

「創建一個新事務,如果當前存在事務,則把當前事務掛起」

所以最終的解決代碼如下

@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("保存地址異常");
    }
}



本文分享自微信公衆號 - Java識堂(erlieStar)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章