目录
1.回顾事务的隔离级别
数据库的事务隔离级别
spring事务隔离级别
2.什么是Spring事务传播机制?
3.设置事务传播级别
PROPAGATION_REQUIRED
PROPAGATION_REQUIRES_NEW
PROPAGATION_NESTED
数据库提供了四种隔离级别供用户选择,包括 READ UNCOMMITTED(读未提交)、READ COMMITTED(读已提交)、REPEATABLE READ(可重复读)、SERIALIZABLE(串行化)。
Spring 框架的事务隔离级别一共有五个,包括:
DEFAULT:表示使用底层数据源的默认隔离级别,一般为数据库的 READ_COMMITTED 隔离级别。
READ_UNCOMMITTED:表示读未提交,最低的隔离级别,不能保证事务的可靠性。
READ_COMMITTED:表示读已提交,能够保证一个事务只会读取到已经提交的数据。
REPEATABLE_READ:表示可重复读,保证在同一个事务中多次读取同一数据时,得到的结果是一致的。
SERIALIZABLE:表示串行化,所有的事务依次顺序执行,可以避免以上三种并发问题。
使用 @Transactional
注解时,可以通过设置 isolation
属性来指定事务的隔离级别,示例如下:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateAccount(int userId, double money) {
// 更新账户余额
}
Spring 框架中的事务传播机制是为了解决多个事务方法之间相互协作的问题,以确保事务操作的一致性和完整性。
在实际的开发过程中,不同的业务逻辑可能会涉及到多个方法的调用,而这些方法有可能会出现嵌套调用的情况。如果不对这些嵌套调用的方法进行事务处理,就有可能会导致数据的不一致或者错误。
假设我们有一个电商系统,涉及到两个业务逻辑:创建订单和扣减库存。其中,创建订单的方法为 createOrder()
,扣减库存的方法为 reduceStock()
。现在,我们需要在一个事务中同时执行这两个方法,以确保订单和库存的一致性。这时就可以使用 Spring 框架中的事务传播机制来管理事务。
假设这两个方法都在 Service 层中实现,且使用注解方式开启事务。我们可以对 createOrder()
方法和 reduceStock()
方法分别设置不同的传播级别,例如:
@Service
@Transactional(rollbackFor = Exception.class)
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private StockDao stockDao;
// REQUIRED 传播级别
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) throws Exception {
// 创建订单
orderDao.insert(order);
// 扣减库存
reduceStock(order.getProductId(), order.getCount());
}
// REQUIRES_NEW 传播级别
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reduceStock(Long productId, int count) throws Exception {
// 扣减库存
stockDao.reduce(productId, count);
}
}
createOrder()
方法使用了 REQUIRED 传播级别,表示如果当前存在事务,则加入该事务进行处理;如果不存在事务,则开启一个新的事务。而 reduceStock()
方法则使用了 REQUIRES_NEW 传播级别,表示每次都开启一个新的事务进行处理。
假设在 createOrder()
方法中出现异常,导致事务回滚,那么 reduceStock()
方法执行的事务也会被回滚;而如果 reduceStock()
方法出现异常,只会回滚该方法中的事务,不会影响到 createOrder()
方法的事务。这样,就可以确保订单和库存的一致性。
Spring 框架中的事务传播机制可以帮助我们管理多个业务逻辑之间的事务,并根据需求设置不同的传播级别,从而确保事务操作的正确性和完整性。
Spring 框架中提供了多种事务传播级别,包括:
PROPAGATION_REQUIRED 默认状态,如果当前没有事务,就新开启一个事务;否则使用当前事务。
PROPAGATION_SUPPORTS 如果当前有事务,就使用该事务;否则不使用事务。
PROPAGATION_MANDATORY 使用当前事务,如果当前不存在事务,就会抛出异常。
PROPAGATION_REQUIRES_NEW 每次都新开启一个事务,且中断当前已经存在的事务。
PROPAGATION_NOT_SUPPORTED 不支持事务,每次都在非事务状态下执行。
PROPAGATION_NEVER 禁止事务,如果当前存在事务,就会抛出异常。
PROPAGATION_NESTED 嵌套事务,如果当前存在事务,则在嵌套事务内执行。如果当前不存在事务,则开启一个新事务。
再看一个关于PROPAGATION_REQUIRED的示例:
当用户表中新增了用户信息时,创建一个日志表记录新增用户的id,time,message
日志表:
mysql> CREATE TABLE `log` (
-> `id` int(11) NOT NULL AUTO_INCREMENT,
-> `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-> `message` text COLLATE utf8mb4_unicode_ci NOT NULL,
-> PRIMARY KEY (`id`)
-> ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
-> ;
ERROR 1050 (42S01): Table 'log' already exists
mysql> select*from log;
Empty set (0.00 sec)
mysql> desc log;
+-----------+-----------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+-----------+------+-----+-------------------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| timestamp | timestamp | NO | | CURRENT_TIMESTAMP | |
| message | text | NO | | NULL | |
+-----------+-----------+------+-----+-------------------+----------------+
3 rows in set (0.01 sec)
用户表:
mysql> select *from userinfo
-> ;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | lisi3 | 456789 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
| 3 | zhangsan | 123456 | | 2023-05-18 17:21:49 | 2023-05-18 17:21:49 | 1 |
| 4 | wangwu | 123456 | | 2023-05-18 17:36:28 | 2023-05-18 17:36:28 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
3 rows in set (0.01 sec)
接下来用到的方法会进行嵌套调用,默认情况下时PROPAGATION_REQUIRED对这些嵌套调用的方法进行事务处理
Spring 事务传播机制的默认值是 REQUIRED。
加入事务:如果当前没有事务,那么被调用的事务方法会开启一个新的事务;如果当前已经有了事务,那么被调用的事务方法会加入到当前事务中,与当前事务同步提交或回滚。
@Data
public class Log {
private int id;
private LocalDateTime timestamp;
private String message;
}
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
}
@RestController
@RequestMapping("/user")
public class Controller3 {
@Autowired
private UserService userService;
@RequestMapping("/add")
@Transactional(propagation = Propagation.REQUIRED)
public int add(String username,String password){
if(null == username || null == password
|| username.equals("") || password.equals("")) return 0;
UserInfo userInfo = new UserInfo();
userInfo.setUsername(username);
userInfo.setPassword(password);
int result = userService.add(userInfo);
return result;
}
}
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED)
public int add(Log log){
int result = logMapper.add(log);
System.out.println("添加日志结果: "+ result);
int num = 100/0;
return result;
}
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private LogService logService;
public int del(Integer id) {
return userMapper.del(id);
}
@Transactional(propagation = Propagation.REQUIRED)
public int add(UserInfo userInfo){
//给用户添加信息
int addUserResult = userMapper.add(userInfo);
System.out.println("添加用户结果: "+ addUserResult);
//给用户添加日志信息
Log log = new Log();
log.setMessage("添加用户信息");
logService.add(log);
return addUserResult;
}
}
insert into log(`message`) values(#{message})
delete from userinfo where id = #{id}
insert into userinfo(username,password) values
(#{username},#{password})
sql执行日志:
数据库中结果:
分析一下原因:
整个执行过程的方法调用链:
异常在logservice出现后进行了事物的回滚。由于使用的是PROPAGATION_REQUIRED对这些嵌套调用的方法进行事务处理
调用链上的方法都加入到了同一个事务中,因此一处出现了异常。整个调用链上的所有方法都会进行回滚。
从而即使添加用户行为没发生异常,但由于其调用了出现异常的日志添加方法,他也跟着回滚了
我们的预期执行结果:在一个调用链上的事务,各自的执行结果相互不干扰
也即在上述例子中,用户添加事务和日志添加事务相互不影响,一个出现异常了,另一个不会跟随着回滚
使用到的传播机制:REQUIRES_NEW
表示当前方法必须开启一个新的事务运行,如果当前已经有事务,则挂起当前事务
改动代码:
对日志方法手动回滚,否则出现异常代码,不处理异常,整个调用链都会报错,感知到后都进行回滚了
将其他方法事务传播机制修改为:
@Transactional(propagation = Propagation.REQUIRES_NEW)
对日志方法手动回滚
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(Log log){
int result = logMapper.add(log);
System.out.println("添加日志结果: "+ result);
//回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
//int num = 100/0;
return result;
}
}
mysql> select*from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | lisi3 | 456789 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
| 3 | zhangsan | 123456 | | 2023-05-18 17:21:49 | 2023-05-18 17:21:49 | 1 |
| 4 | wangwu | 123456 | | 2023-05-18 17:36:28 | 2023-05-18 17:36:28 | 1 |
| 20 | zhaoliu | 123456 | | 2023-05-31 11:31:51 | 2023-05-31 11:31:51 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
4 rows in set (0.00 sec)
mysql> select*from log;
Empty set (0.00 sec)
将REQUIRES_NEW换为REQUIRED 默认状态执行又会出现报错且全部回滚的情况
报错: Transaction rolled back because it has been marked as rollback-only
内部事务回滚,外部事务也会回滚,但会报异常
如果外部事务回滚,内部事务会跟着回滚,不会报异常
数据库表还是没有任何改变,全部回滚
嵌套事务:PROPAGATION_NESTED ,如果当前存在事务,则在嵌套事务内执行。如果当前不存在事务,则开启一个新事务。
@Transactional(propagation = Propagation.NESTED)将日志手动回滚
mysql> select*from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | lisi3 | 456789 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
| 3 | zhangsan | 123456 | | 2023-05-18 17:21:49 | 2023-05-18 17:21:49 | 1 |
| 4 | wangwu | 123456 | | 2023-05-18 17:36:28 | 2023-05-18 17:36:28 | 1 |
| 20 | zhaoliu | 123456 | | 2023-05-31 11:31:51 | 2023-05-31 11:31:51 | 1 |
| 23 | zhaoliu2 | 123456 | | 2023-05-31 12:33:36 | 2023-05-31 12:33:36 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
5 rows in set (0.00 sec)
mysql> select*from log;
Empty set (0.00 sec)
结果:只回滚日志事务,用户添加事务没有回滚
嵌套事务和加入事务的区别
两种方式的区别主要在于事务的隔离级别和提交方式: