Spring 中事务的实现
Spring 中的事务操作分为两类:
- 编程式事务(手动写代码操作事务)
- 声明式事务(利用注解自动开启和提交事务)
编程式事务
Spring 手动操作事务和MySQL 操作事务类似,它也是有 3 个重要操作步骤:
– 开启事务 start transaction;
//业务执行
– 提交事务 commit;
– 回滚事务 rollback;
SpringBoot 内置了两个对象,DataSourceTransactionManager
用来获取事务(开启事务)、提交或回滚事务的,而TransactionDefinition
是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从而获得⼀个事务transaction。
@Slf4j
@RequestMapping("/trans")
@RestController
public class TransactionController {
@Autowired
private UserService userService;
//数据库的事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
private TransactionDefinition transactionDefinition;
@RequestMapping("/addUser")
public Integer addUser(String username,String password){
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
User user = new User(username,password);
Integer result = userService.insert(user);
log.info("影响行数" + result);
//事务提交
dataSourceTransactionManager.commit(transaction);
//事务回滚
// dataSourceTransactionManager.rollback(transaction);
return result;
}
}
声明式事务
以上代码虽然可以实现事务,但操作很繁琐。而声明式事务只需要在需要的方法上添加 @Transactional
注解就可以实现了,⽆需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务。
@Slf4j
@RequestMapping("/trans2")
@RestController
public class TransactionController2 {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/addUser")
public Integer addUser(String username,String password){
User user = new User(username,password);
Integer result = userService.insert(user);
log.info("影响行数" + result);
//执行了异常代码
int a = 10/0;
return result;
}
}
在上述代码中,添加@Transactional注解,如果程序执行成功,自动提交事务,如果程序执行异常,则自动回滚事务。如果不加注解,在代码执行异常时不会回滚。
@Transactional的作用范围
修饰方法:需要注意只能应用到 public 方法上,否则不生效;
修饰类:表明该注解对该类中所有的 public 方法都生效。
@Transactional的参数
比如在上述代码中,可以指定出现算数异常时,不回滚:
@Transactional(noRollbackFor = ArithmeticException.class)
@RequestMapping("/addUser")
public Integer addUser(String username,String password){
User user = new User(username,password);
Integer result = userService.insert(user);
log.info("影响行数" + result);
//执行了异常代码
int a = 10/0;
return result;
}
注意:@Transactional默认只有Error和运行时异常(RuntimeException及其子类)才会自动回滚,其他异常不会自动回滚。所以如果想要在所有异常情况下都进行回滚,可以指定参数
rollbackFor=Excepion.class
注意事项
@Transactional 在异常被捕获的情况下,不会进行事务自动回滚。验证以下代码是否会发生事务回滚:
@Transactional
@RequestMapping("/addUser3")
public Integer addUser3(String username,String password){
User user = new User(username,password);
Integer result = userService.insert(user);
log.info("影响行数" + result);
try{
//执行了异常代码
int a = 10/0;
}catch (Exception e){
e.printStackTrace();
}
return result;
}
打印日志可以发现事务没有自动回滚,而是提交了:
解决方案:
方案1:对于捕获的异常,事务是会自动回滚的,因此解决方案1就是可以将异常重新抛出;
方案2:手动回滚事务,在方法中使用 TransactionAspectSupport.currentTransactionStatus() 可以得到当前的事务,然后设置回滚方法 setRollbackOnly 就可以实现回滚了。
事务隔离级别
事务隔离级别解决的是多个事务同时调用⼀个数据库的问题,为了防止其他的事务影响当前事务执行的⼀种策略。如下图所示:
MySQL的事务隔离级别
在MySQL中,事务的隔离级别有4种:
Spring中事务的隔离级别
Spring 中事务隔离级别包含以下 5 种:
1.Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
2.Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
3.Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。
4.Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。
5.Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低。
相比于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个Isolation.DEFAULT(以所连接的数据库的事务隔离级别为主)。Spring 中事务隔离级别可以通过 @Transactional 中的 isolation
属性进行设置,具体操作如下图所示:
@Transactional(isolation = Isolation.SERIALIZABLE)
Spring事务传播机制
Spring 事务传播机制定义了多个包含了事务的方法相互调用时,事务是如何在这些⽅法间进行传递的。事务传播机制是保证⼀个事务在多个调用方法间的可控性的(稳定性的)。 所以事务传播机制解决的是⼀个事务在多个节点(方法)中传递的问题,如下图所示:
事务传播机制有哪些
Spring 事务传播机制包含以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建⼀个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
以上 7 种传播机制,可以根据是否支持当前事务分为以下 3 类:
Spring 事务传播机制使用和各种场景演示
以用户注册功能为例:
- 插入用户表数据
- 插入日志表数据
支持当前事务:REQUIRED
先开启事务先成功插⼊⼀条用户数据,然后再执行异常的日志:
UserService:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> selectAllUser() {
return userMapper.queryAll();
}
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(User user){
return userMapper.insert(user);
}
}
UserLogService:
@Service
public class UserLogService {
@Resource
private UserLogMapper userLogMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertLog(UserLog userLog){
Integer result = userLogMapper.insertLog(userLog);
//出现异常
int a = 10/0;
return result;
}
}
Controller:
@Slf4j
@RequestMapping("/trans3")
@RestController
public class TransactionController3 {
@Resource
private UserService userService;
@Resource
private UserLogService userLogService;
@Transactional
@RequestMapping("/addUser")
public boolean addUser( String username,String password){
//1.插入用户表
User user = new User(username,password);
userService.insert(user);
//2.插入日志表
UserLog userLog = new UserLog(username);
userLogService.insertLog(userLog);
return true;
}
}
执行结果:程序报错,数据库没有插入任何数据。
执行流程描述:
1.UserService 中的保存⽅法正常执行完成。
2.UserLogService 插入日志程序报错,因为使用的是 Controller 中的事务,所以整个事务回滚。
3.数据库中没有插入任何数据,也就是步骤 1 中的用户插⼊方法也回滚了。
不支持当前事务:REQUIRES_NEW
将Controller 类中的代码不变,将添加用户和添加日志的方法修改为 REQUIRES_NEW,表示 不支持当前事务,重新创建事务,观察执行结果:
UserService:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> selectAllUser() {
return userMapper.queryAll();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer insert(User user){
return userMapper.insert(user);
}
}
UserLogService:
@Service
public class UserLogService {
@Resource
private UserLogMapper userLogMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer insertLog(UserLog userLog){
Integer result = userLogMapper.insertLog(userLog);
//出现异常
int a = 10/0;
return result;
}
}
通过日志可以发现,用户表中成功插入了数据,日志表执行失败,但没影响 UserController 中的事务。原因在于当UserLog出现异常时,造成了事务回滚,但由于此时重新创建了事务,所以不影响userinfo表。
不支持当前事务:NEVER 抛异常
将UserService中的事务的传播机制改为Propagation.NEVER,其余不变:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> selectAllUser() {
return userMapper.queryAll();
}
@Transactional(propagation = Propagation.NEVER)
public Integer insert(User user){
return userMapper.insert(user);
}
}
执行结果直接抛出异常:
NESTED 嵌套事务
UserService 实现代码如下:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> selectAllUser() {
return userMapper.queryAll();
}
@Transactional(propagation = Propagation.NESTED)
public Integer insert(User user){
return userMapper.insert(user);
}
}
UserLogService实现如下:
@Service
public class UserLogService {
@Resource
private UserLogMapper userLogMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insertLog(UserLog userLog){
Integer result = userLogMapper.insertLog(userLog);
//出现异常
int a = 10/0;
return result;
}
}
Controller:
@Slf4j
@RequestMapping("/trans3")
@RestController
public class TransactionController3 {
@Resource
private UserService userService;
@Resource
private UserLogService userLogService;
@Transactional
@RequestMapping("/addUser")
public boolean addUser( String username,String password){
//1.插入用户表
User user = new User(username,password);
userService.insert(user);
//2.插入日志表
UserLog userLog = new UserLog(username);
userLogService.insertLog(userLog);
return true;
}
}
从结果来看,当执行没有异常时,会正常在用户表和日志表中插入数据;而执行存在异常时,两个表中都不会插入数据(日志中没有commit)。从结果上来看,貌似嵌套事务和加入事务没有区别。
分析执行流程描述:
1.UserController 中调用了 UserService 的添加方法,UserService 使用 NESTED 修饰循环嵌套了UserController 的事务,并成功执行了添加方法。
2.UserService 中调用了 UserLogService 的添加方法,UserLogService 使用 NESTED 修饰循环嵌套了上⼀个调用类的事务,执行了添加方法,但在执行的过程中出现了异常,回顾当前事务,日志添加失败。
3.因为是嵌套事务,所以日志添加回顾之后,往上找调用它的方法和事务回滚了用户添加,所以用户添加也失败了,所以最终的结果是用户表和日志表都没有添加任何数据。
嵌套事务和加入事务的区别
在UserLogService中,当出现异常时,手动回滚事务:
@Service
public class UserLogService {
@Resource
private UserLogMapper userLogMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insertLog(UserLog userLog){
Integer result = userLogMapper.insertLog(userLog);
try {
//出现异常
int a = 10/0;
}catch (Exception e){
//回滚当前事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
}
从日志的打印结果来看,只有一个commit。最终的执行结果是用户表中成功添加数据,而日志表中没有添加数据。这说明:日志中的事务已经回滚,但是嵌套事务不会回滚嵌套之前的事务,也就是说嵌套事务可以实现部分事务回滚。
嵌套事务只所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念,嵌套事务进入之后相当于新建了⼀个保存点,而回滚时只回滚到当前保存点,因此之前的事务是不受影响。而 REQUIRED 是加入到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚。 在 MySQL 的官方文档汇总找到相应的资料。
嵌套事务(NESTED)和加⼊事务(REQUIRED )的区别:
1.整个事务如果全部执行成功,⼆者的结果是⼀样的;
2.如果事务执行到⼀半失败了,那么REQUIRED 会让整个事务全部回滚;而嵌套事务会局部回滚,不会影响上⼀个方法中执行的结果。
继续加油~