前言
当我们使用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类似的操作。 |
搭建测试环境
- 框架使用的是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>
- 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语句打印
- 启动类包扫描配置:
@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事务。
- 数据库表设计:
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');
用户表和账户表的对应关系是一对一关系。
- 单元测试类:
@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("异常回滚");
}
}
事务注解加在本地方法,并抛出异常回滚。转账前:
转账后:
可以看到,账户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("异常回滚");
}
}
转账前:
转账后:
转账成功,说明事务回滚失败,事务加在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("异常回滚");
}
}
这里把事务加在了转账方法上,然后在该方法对异常进行了捕获,转账前:
转账后:
转账成功,说明事务回滚失败,事务方法进行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) 上也添加了事务,转账前:
转账后:
发现数据并没有变化,说明事务生效并回滚了,这里跟前面事务失效场景的区别就在于抛出异常的方法上添加了事务注解,这里就触发了事务的传播机制:
当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("异常回滚");
}
}
转账前:
转账后:
可以看到,方法 reduceBalance(Long fromId, BigDecimal amount) 没有回滚,方法 increaseBalance(Long toId, BigDecimal amount) 回滚了,这里使用到了事务属性 PROPAGATION_REQUIRES_NEW:
若当前没有事务,则新建一个事务。若当前存在事务,则新建一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。
两个方法使用了独立的事务,所以出现上面这样的情况。