spring中事务的传播行为详解+业务场景示例

1.事务传播行为列举

事务传播行为有7种,不是与数据库交互,数据库定义的,而是spring框架自带的,是spring通过aop实现的,下边是7种事务传播行为:

REQUIRED:如果当前没有事务,就新建一个事务。如果当前存在事务,则加入这个事务。 

NESTED:如果当前没有事务,就新建一个事务。如果当前事务存在,则执行一个嵌套事务。

REQUIRES_NEW:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。 

SUPPORTS:如果当前没有事务,就以非事务方式执行。 如果当前有事务,则使用事务。

NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

MANDATORY:以事务方式执行,如果当前没有事务,就抛出异常。 

NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。 

我们最常用的是前三个REQUIRED、NESTED、REQUIRES_NEW,下边我们用一系列业务场景来详解这三个事务传播行为。

2.事务传播行为场景示例

2.1业务场景1

首先有这样一个场景,我们有个注册业务,注册时需要记录登录账号、密码、身份证号、姓名、手机号这5个信息,涉及三张表,分别是t_user_account、t_user_idcard、t_user_phone,刚开始的业务要求这些都不能为空,如果其中一个发生异常,那么其余已经插入的全部回滚(这些异常是我用作数据校验时抛出的,正常生产上会先进行数据校验,校验成功了再更改数据库,我这里为了演示@Transactional的回滚效果,先更改数据,如果校验不通过,便会通过spring的事务进行回滚)。

2.2业务代码实现

spring bean配置文件

我们在bean配置文件中开启事务

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--3  1.组件扫描 2.引入外部属性文件 3.配置数据源 4.创建jdbcTemplate对象,注入数据源-->
    <!--组件扫描-->
    <context:component-scan base-package="com.atguigu.spring6.txSpread"></context:component-scan>
    <!--引入外部属性文件,创建数据源对象-->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <!--配置数据源-->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="username" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>
    <!--创建jdbcTemplate对象,注入数据源-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>

    <!--
        开启事务的注解驱动
        通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
    -->
    <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

VO

public class UserVO {
    private Long id;
    private String userAccount;
    private String userPwd;
    private String phoneNum;
    private String userName;
    private String IDCard;

    public UserVO(Long id, String userAccount, String userPwd, String phoneNum, String userName, String IDCard) {
        this.id = id;
        this.userAccount = userAccount;
        this.userPwd = userPwd;
        this.phoneNum = phoneNum;
        this.userName = userName;
        this.IDCard = IDCard;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserAccount() {
        return userAccount;
    }

    public void setUserAccount(String userAccount) {
        this.userAccount = userAccount;
    }

    public String getUserPwd() {
        return userPwd;
    }

    public void setUserPwd(String userPwd) {
        this.userPwd = userPwd;
    }

    public String getPhoneNum() {
        return phoneNum;
    }

    public void setPhoneNum(String phoneNum) {
        this.phoneNum = phoneNum;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getIDCard() {
        return IDCard;
    }

    public void setIDCard(String IDCard) {
        this.IDCard = IDCard;
    }
}

controller

register()方法用于实现这个业务,分别调用userService的insertId()方法用于插入一条新记录的主键和register()方法用于插入登录账号、密码、手机号、身份证号、姓名。

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 注册用户
     * @param userVO
     */
    public void register(UserVO userVO) {
        userService.insertId(userVO);
        try {
            userService.register(userVO);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

service

public interface UserService {
    //插入新用户的id
    void insertId(UserVO userVO);
    //插入用户账号、密码、手机号、姓名、身份证号
    void register(UserVO userVO);
}

serviceImpl

这里主要说register方法,在register方法中,我们调用userDao中的insertAccountAndPwd方法和insertIDcardAndName方法分别进行账号密码的插入与身份证号、姓名的插入,在register方法中直接调用jdbcTemplate进行手机号的插入。

这里有一点需要说明,我这里选择抛出的是RuntimeException,该异常是运行时异常,如果不做异常回滚的配置,默认非运行时异常(比如我刚开始抛出的是Exception),则不会发生回滚。

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private UserDao userDao;

    @Override
    public void insertId(UserVO userVO) {

        String sql1 = "insert into t_user_account (user_id) values (?)";
        jdbcTemplate.update(sql1,userVO.getId());
        String sql2 = "insert into t_user_idcard (user_id) values (?)";
        jdbcTemplate.update(sql2,userVO.getId());
        String sql3 = "insert into t_user_phone (user_id) values (?)";
        jdbcTemplate.update(sql3,userVO.getId());
    }

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void register(UserVO userVO) {
        //插入账号密码
        userDao.insertAccountAndPwd(userVO);
        //再插入手机号
        String sql1 = "update t_user_phone set phone_num = ? where user_id = ?";
        jdbcTemplate.update(sql1,userVO.getPhoneNum(),userVO.getId());
        if (userVO.getPhoneNum() == null) {
            throw new RuntimeException("手机号码不能为空");
        }
        //插入身份证号和姓名
        userDao.insertIDcardAndName(userVO);
    }
}

dao

public interface UserDao {
    //插入账户和密码
    void insertAccountAndPwd(UserVO userVO);

    //插入身份证号和姓名
    void insertIDcardAndName(UserVO userVO);

}

daoImpl

​
@Repository
public class UserDaoImpl implements UserDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void insertAccountAndPwd(UserVO userVO) {

        String sql1 = "update t_user_account set user_account = ?, user_pwd = ? where user_id = ?";
        jdbcTemplate.update(sql1,userVO.getUserAccount(),userVO.getUserPwd(),userVO.getId());
        if (userVO.getUserAccount() == null) {
            throw new RuntimeException("账号不能为空");
        }
        if (userVO.getUserPwd() == null) {
            throw new RuntimeException("密码不能为空");
        }
    }

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void insertIDcardAndName(UserVO userVO) {
        //先将新用户的身份证号和姓名插入

        String sql1 = "update t_user_idcard set ID_card = ?, user_name = ? where user_id = ?";
        jdbcTemplate.update(sql1,userVO.getIDCard(),userVO.getUserName(),userVO.getId());
        if (userVO.getIDCard() == null) {
            throw new RuntimeException("身份证号不能为空");
        }
        if (userVO.getUserName() == null) {
            throw new RuntimeException("姓名不能为空");
        }
    }
}

​

测试方法

@SpringJUnitConfig(locations = "classpath:txSpreadBean.xml")
public class TestTxSpread {

    @Autowired
    private UserController userController;

    @Test
    public void test2() {
        UserVO userVO1 = new UserVO(1L,"花花","123456",null,"花木兰","511387197807180001");
        userController.register(userVO1);

        UserVO userVO2 = new UserVO(2L,"花花","123456","13612312345","花木兰",null);
        userController.register(userVO2);
    }
}

2.3内围方法与外围方法定义

这里我先来给大家规整一下代码,涉及事务传播行为要点的是UserServiceImpl的register方法,这里为了后续方便指认,我就称UserServiceImpl的register方法为外围方法,而它先调用了UserDaoImpl的insertAccountAndPwd方法进行账号和密码的插入,然后在register方法中直接调用jdbcTemplate进行手机号的插入,最后再调用userDao中的insertIDcardAndName方法进行身份证号、姓名的插入,这里我就称UserDaoImpl的insertAccountAndPwd方法和insertIDcardAndName方法为两个内围方法。

register方法:是外围方法,除了调用了insertAccountAndPwd方法和insertIDcardAndName方法外,直接调用jdbcTemplate进行手机号的插入。

insertAccountAndPwd方法:是内围方法,进行账号密码的插入。

insertIDcardAndName方法:是内围方法,进行身份证号、姓名的插入。

2.4业务场景1实现及测试

当前业务需求:

我们要实现登录账号、密码、手机号、身份证号、姓名,这些都不能为空,如果其中一个发生异常,那么其余已经插入的全部回滚。

根据这个业务及我们当前代码结构,我们需要在这一个外围方法和两个内围方法中都添加@Transactional注解,因为要求有异常,只要有数据库改动的代码全部回滚。

事务传播行为选择:

这里我们将@Transactional的事务传播属性propagation值设为Propagation.REQUIRED,这个值我们说过了是默认的,同时它的含义是:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这正好符合我们业务需求:外围方法已经是一个事务后,被调用的两个内围方法也加入到这个事务中,就实现了只要有任意一个异常,只会插入一个id,其余需要插入的登录账号、密码、手机号、身份证号、姓名都回滚。

测试:

上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现只要一个方法内有异常,这三个方法都会回滚,数据库里没有一个表中有新记录。

2.5业务场景2实现及测试

当前业务需求:

接下来,我们业务逻辑改为注册时可以不提交身份证号与姓名(但是身份证号和姓名必须同时添加或不添加),后续确定后再提交即可,账号密码和手机号仍然不可缺少,少一个则全部回滚。

那么这里我们仍保持内围方法insertIDcardAndName方法为事务,外围方法register和内围方法insertAccountAndPwd也为事务,但是要保证insertIDcardAndName方法出现异常时不能导致外围方法回滚,导致外围回滚有两种途径,一个是内围方法出现异常后会抛出给外围方法,然后外围方法的@Transactional感知到异常回滚,另一个是外围方法与内围方法本事是一个事务,那么内围方法回滚的时候自然会使外围方法也会滚。

事务传播行为选择:

所以这里我们首先要在外围方法中将内围方法insertIDcardAndName的异常捕获,接着不能再使用REQUIRED为事务传播属性了,因为内围方法事务传播属性为REQUIRED代表加入到外围方法的事务中,会同时回滚,可以将@Transactional的事务传播属性propagation值设为Propagation.NESTED,它的含义是:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,就新建一个事务。这正好符合我们业务需求:外围方法是一个事务的情况下,我们的内围方法有异常只需要回滚自己就行,外围方法有异常才回滚全部。

测试:

上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现当insertIDcardAndName方法有异常,并不会导致外围方法register和内围方法insertAccountAndPwd回滚。

2.6业务场景3实现及测试

当前业务需求:

接下来,我们业务逻辑改为注册时可以不提交身份证号与姓名(但是身份证号和姓名必须同时添加或不添加),也可以不提交手机号,后续确定后再提交即可,账号密码和手机号仍然不可缺少,少一个则全部回滚。

事务传播行为选择:

上次业务修改我们只要保证内围方法insertIDcardAndName回滚不带上外围方法回滚,这次我们还要保证外围方法register回滚时不要带上内围方法insertAccountAndPwd回滚,所以这次我们可以将@Transactional的事务传播属性propagation值设为Propagation.REQUIRES_NEW,它的含义是:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。这正好符合我们业务需求:外围方法是一个事务的情况下,我们的内围方法开始它自己的业务,外围方法回滚不带上内围方法。

测试:

上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现当外围方法register方法有异常,并不会导致内围方法insertAccountAndPwd回滚。

3总结

当我们不想让内围回滚带上外围也回滚,内围方法事务就不要使用REQUIRED,可以使用NESTED或REQUIRES_NEW。

当我们不想让外围回滚带上内围也回滚,内围方法事务就不要使用REQUIRED和NESTED,可以使用REQUIRES_NEW。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring事务传播行为指的是一个事务方法调用另一个事务方法时,事务如何传播行为Spring框架提供了7种事务传播行为,分别是REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER和NESTED。 1. REQUIRED 如果当前没有事务,则创建一个新事务。如果当前存在事务,则加入该事务。这是默认的传播行为示例代码: ```java @Transactional(propagation = Propagation.REQUIRED) public void method1() { // 业务逻辑 method2(); // 调用method2方法 } @Transactional(propagation = Propagation.REQUIRED) public void method2() { // 业务逻辑 } ``` 在method1方法调用method2方法时,如果method2方法没有事务,则会加入method1方法的事务。 2. SUPPORTS 如果当前有事务,则加入该事务。如果当前没有事务,则以非事务方式执行。 示例代码: ```java @Transactional(propagation = Propagation.REQUIRED) public void method1() { // 业务逻辑 method2(); // 调用method2方法 } @Transactional(propagation = Propagation.SUPPORTS) public void method2() { // 业务逻辑 } ``` 在method1方法调用method2方法时,如果method1方法有事务,则method2方法会加入method1方法的事务。如果method1方法没有事务,则method2方法会以非事务方式执行。 3. MANDATORY 如果当前有事务,则加入该事务。如果当前没有事务,则抛出异常。 示例代码: ```java @Transactional(propagation = Propagation.REQUIRED) public void method1() { // 业务逻辑 method2(); // 调用method2方法 } @Transactional(propagation = Propagation.MANDATORY) public void method2() { // 业务逻辑 } ``` 在method1方法调用method2方法时,如果method1方法有事务,则method2方法会加入method1方法的事务。如果method1方法没有事务,则会抛出异常。 4. REQUIRES_NEW 如果当前有事务,则挂起该事务,并创建一个新事务。如果当前没有事务,则创建一个新事务示例代码: ```java @Transactional(propagation = Propagation.REQUIRED) public void method1() { // 业务逻辑 method2(); // 调用method2方法 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void method2() { // 业务逻辑 } ``` 在method1方法调用method2方法时,如果method1方法有事务,则method2方法会挂起method1方法的事务,并创建一个新事务。如果method1方法没有事务,则method2方法会创建一个新事务。 5. NOT_SUPPORTED 以非事务方式执行。如果当前存在事务,则挂起该事务示例代码: ```java @Transactional(propagation = Propagation.REQUIRED) public void method1() { // 业务逻辑 method2(); // 调用method2方法 } @Transactional(propagation = Propagation.NOT_SUPPORTED) public void method2() { // 业务逻辑 } ``` 在method1方法调用method2方法时,如果method1方法有事务,则method2方法会以非事务方式执行。如果method1方法没有事务,则method2方法也会以非事务方式执行。 6. NEVER 以非事务方式执行。如果当前存在事务,则抛出异常。 示例代码: ```java @Transactional(propagation = Propagation.REQUIRED) public void method1() { // 业务逻辑 method2(); // 调用method2方法 } @Transactional(propagation = Propagation.NEVER) public void method2() { // 业务逻辑 } ``` 在method1方法调用method2方法时,如果method1方法有事务,则会抛出异常。如果method1方法没有事务,则method2方法会以非事务方式执行。 7. NESTED 如果当前有事务,则在该事务嵌套一个新事务,如果当前没有事务,则创建一个新事务示例代码: ```java @Transactional(propagation = Propagation.REQUIRED) public void method1() { // 业务逻辑 method2(); // 调用method2方法 } @Transactional(propagation = Propagation.NESTED) public void method2() { // 业务逻辑 } ``` 在method1方法调用method2方法时,如果method1方法有事务,则在该事务嵌套一个新事务。如果method1方法没有事务,则method2方法会创建一个新事务

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值