Spring事务需要注意的几个场景

前言

当我们使用spring的时候,通常会将事务交给Spring来控制,Spring事务是使用 aop 切面实现的,我们不用关心事务的开始、提交、回滚,只需要在方法上加 @Transactional 注解,这时候我们就需要注意一些踩坑的点,下面我就结合测试用例来总结一下。

Spring事务传播机制

Spring的事务注解 @Transactional 提供 propagation 属性来对事务事务的传播行为进行控制。

属性说明
PROPAGATION_REQUIRED支持当前事务,如果没有事务会创建一个新的事务
PROPAGATION_SUPPORTS支持当前事务,如果没有事务的话以非事务方式执行
PROPAGATION_MANDATORY支持当前事务,如果没有事务抛出异常
PROPAGATION_REQUIRES_NEW创建一个新的事务并挂起当前事务
PROPAGATION_NOT_SUPPORTED以非事务方式执行,如果当前存在事务则将当前事务挂起
PROPAGATION_NEVER以非事务方式进行,如果存在事务则抛出异常
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

搭建测试环境

  1. 框架使用的是Springboot+mybatis-plus+mysql,maven依赖如下:
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.2</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
  1. yaml配置:
server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?tuseUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启SQL语句打印
  1. 启动类包扫描配置:
@EnableTransactionManagement
@SpringBootApplication(scanBasePackages = "cn.com.mybatis")
@MapperScan("cn.com.mybatis.mapper")
public class LearnMybatisApplication {

    public static void main(String[] args) {
        SpringApplication.run(LearnMybatisApplication.class, args);
    }
}

其中 @EnableTransactionManagement 开启了Spring事务。

  1. 数据库表设计:
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
                        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
                        `name` varchar(30) DEFAULT NULL COMMENT '姓名',
                        `age` int(11) DEFAULT NULL COMMENT '年龄',
                        `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
                        `manager_id` bigint(20) DEFAULT NULL COMMENT '直属上级id',
                        `create_time` datetime DEFAULT NULL COMMENT '创建时间',
                        PRIMARY KEY (`id`),
                        KEY `manager_fk` (`manager_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户表';

INSERT INTO user (name, age ,email, manager_id, create_time) VALUES
('张老板', 40, 'boss@mybatis.com', NULL, '2021-05-22 09:48:00'),
('李经理', 35, 'manager@mybatis.com', 1, '2021-04-22 09:48:00'),
('黄主管', 30, 'supervisor@mybatis.com', 2, '2021-04-22 09:48:00'),
('吴组长', 27, 'leader@mybatis.com', 2, '2021-05-22 09:48:00'),
('小菜', 23, 'cai@mybatis.com', 2, '2021-05-22 09:48:00');

DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
                           `id` bigint(20) NOT NULL AUTO_INCREMENT,
                           `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
                           `balance` decimal(10,2) DEFAULT NULL COMMENT '余额',
                           `create_time` datetime DEFAULT NULL COMMENT '创建时间',
                           PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='账户表';

INSERT INTO `account`(`user_id`, `balance`, `create_time`) VALUES
(1, 1500.00, '2021-06-17 00:03:42'),
(2, 2000.00, '2021-06-17 00:03:46'),
(3, 2000.00, '2021-06-17 00:04:26'),
(4, 1100.00, '2021-06-17 00:04:33'),
(5, 900.00, '2021-06-17 00:04:29');

用户表和账户表的对应关系是一对一关系。

  1. 单元测试类:
@RunWith(SpringRunner.class)
@DisplayName("账户接口测试")
@SpringBootTest
public class AccountServiceTest {

    private static final Logger logger = LoggerFactory.getLogger(AccountServiceTest.class);

    @Autowired
    private IAccountService accountService;

    @Test
    @DisplayName("转账测试")
    public void testTransfer() {
        Account accountFrom = accountService.getById(1);
        Account accountTo = accountService.getById(2);
        logger.info("账户A余额:" + accountFrom.getBalance());
        logger.info("账户B余额:" + accountTo.getBalance());

        System.out.println("账户B给账户A转账:500");
        accountService.transfer(2L, 1L, new BigDecimal(500));

        accountFrom = accountService.getById(1);
        accountTo = accountService.getById(2);
        logger.info("转账后账户A余额:" + accountFrom.getBalance());
        logger.info("转账后账户B余额:" + accountTo.getBalance());
    }
}

OK,测试环境就搭建完成了,这里写了一个账户余额转账的接口,账户B向账户A转账500。

不支持事务的场景

本地方法直接调用

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {


    @Override
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        reduceBalance(fromId, amount);
        increaseBalance(toId, amount);
    }

    /**
     * 扣减转账方余额
     *
     * @param fromId
     * @param amount
     */
    public void reduceBalance(Long fromId, BigDecimal amount) {
        Account fromAccount = this.getById(fromId);
        fromAccount.setId(fromId);
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        this.updateById(fromAccount);
    }

    /**
     * 增加收款方余额
     *
     * @param toId
     * @param amount
     */
    @Transactional(rollbackFor = Exception.class)
    public void increaseBalance(Long toId, BigDecimal amount) {
        Account toAccount = this.getById(toId);
        toAccount.setId(toId);
        toAccount.setBalance(toAccount.getBalance().add(amount));
        this.updateById(toAccount);
        throw new RuntimeException("异常回滚");
    }
}

事务注解加在本地方法,并抛出异常回滚。转账前:

image.png
转账后:

image.png
可以看到,账户A多了500,账户B少了500,转账成功,说明事务回滚失败,事务加在本地方法失效。

非public方法

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {

    @Autowired
    private AccountService accountService;

    @Override
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        reduceBalance(fromId, amount);
        accountService.increaseBalance(toId, amount);
    }

    @Override
    public void reduceBalance(Long fromId, BigDecimal amount) {
        Account fromAccount = this.getById(fromId);
        fromAccount.setId(fromId);
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        this.updateById(fromAccount);
    }

    @Override
    public void increaseBalance(Long toId, BigDecimal amount) {
        Account toAccount = this.getById(toId);
        toAccount.setId(toId);
        toAccount.setBalance(toAccount.getBalance().add(amount));
        this.updateById(toAccount);
        throw new RuntimeException("异常回滚");
    }
}

这里使用Spring注入的方式调用增加了事务注解的非public方法:

@Service
public class AccountService {

    @Autowired
    private IAccountService iAccountService;

    @Transactional(rollbackFor = Exception.class)
    protected void increaseBalance(Long toId, BigDecimal amount) {
        Account toAccount = iAccountService.getById(toId);
        toAccount.setId(toId);
        toAccount.setBalance(toAccount.getBalance().add(amount));
        iAccountService.updateById(toAccount);
        throw new RuntimeException("异常回滚");
    }
}

转账前:

image.png
转账后:

image.png
转账成功,说明事务回滚失败,事务加在Spring管理的Bean的非public方法失效。

try…catch…语句块捕获未配事务方法

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {

    @Autowired
    private IAccountService iAccountService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        try {
            reduceBalance(fromId, amount);
            iAccountService.increaseBalance(toId, amount);
        } catch (Exception e) {

        }
    }

    @Override
    public void reduceBalance(Long fromId, BigDecimal amount) {
        Account fromAccount = this.getById(fromId);
        fromAccount.setId(fromId);
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        this.updateById(fromAccount);
    }

    @Override
    public void increaseBalance(Long toId, BigDecimal amount) {
        Account toAccount = this.getById(toId);
        toAccount.setId(toId);
        toAccount.setBalance(toAccount.getBalance().add(amount));
        this.updateById(toAccount);
        throw new RuntimeException("异常回滚");
    }
}

这里把事务加在了转账方法上,然后在该方法对异常进行了捕获,转账前:

image.png
转账后:

image.png
转账成功,说明事务回滚失败,事务方法进行try…catch…语句块捕获后事务失效。

支持事务的场景

try…catch…语句块捕获已配事务方法

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {

    @Autowired
    private IAccountService iAccountService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        try {
            reduceBalance(fromId, amount);
            iAccountService.increaseBalance(toId, amount);
        } catch (Exception e) {

        }
    }

    @Override
    public void reduceBalance(Long fromId, BigDecimal amount) {
        Account fromAccount = this.getById(fromId);
        fromAccount.setId(fromId);
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        this.updateById(fromAccount);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void increaseBalance(Long toId, BigDecimal amount) {
        Account toAccount = this.getById(toId);
        toAccount.setId(toId);
        toAccount.setBalance(toAccount.getBalance().add(amount));
        this.updateById(toAccount);
        throw new RuntimeException("异常回滚");
    }
}

这里把事务加在了转账方法上,并且方法 increaseBalance(Long toId, BigDecimal amount) 上也添加了事务,转账前:
image.png
转账后:
image.png
发现数据并没有变化,说明事务生效并回滚了,这里跟前面事务失效场景的区别就在于抛出异常的方法上添加了事务注解,这里就触发了事务的传播机制:

当B方法有事务且事务传播机制为REQUIRED时,会和A方法的事务合并成一个事务,此时B方法发生异常,spring捕获异常后,事务将会被设置全局rollback,而最外层的事务方法执行commit操作,这时由于事务状态为rollback,spring认为不应该commit提交该事务,就会回滚该事务,这就是为什么A方法的事务也被回滚了。

如果将方法 increaseBalance(Long toId, BigDecimal amount) 的事务属性配置为 PROPAGATION_REQUIRES_NEW 会怎样,我们来看下:

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {

    @Autowired
    private IAccountService iAccountService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        try {
            reduceBalance(fromId, amount);
            iAccountService.increaseBalance(toId, amount);
        } catch (Exception e) {

        }
    }

    @Override
    public void reduceBalance(Long fromId, BigDecimal amount) {
        Account fromAccount = this.getById(fromId);
        fromAccount.setId(fromId);
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        this.updateById(fromAccount);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void increaseBalance(Long toId, BigDecimal amount) {
        Account toAccount = this.getById(toId);
        toAccount.setId(toId);
        toAccount.setBalance(toAccount.getBalance().add(amount));
        this.updateById(toAccount);
        throw new RuntimeException("异常回滚");
    }
}

转账前:
image.png
转账后:

image.png
可以看到,方法 reduceBalance(Long fromId, BigDecimal amount) 没有回滚,方法 increaseBalance(Long toId, BigDecimal amount) 回滚了,这里使用到了事务属性 PROPAGATION_REQUIRES_NEW

若当前没有事务,则新建一个事务。若当前存在事务,则新建一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。

两个方法使用了独立的事务,所以出现上面这样的情况。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值