目录
一.简单理解事务
事务是⼀组操作的集合,是⼀个不可分割的操作。
事务会把一组操作作为一个整体,这组中的全部操作要么同时成功,要么同时失败,不允许一部分成功一部分失败的情况。就拿现实生活中大家玩游戏来举例:
游戏里面有许多充值道具,诸如王者荣耀里面英雄的皮肤,瓦洛兰特里面枪械的皮肤,对于这部分游戏内容我们需要充钱才能获得,在充钱的过程中不知道大家有没有担心过这样一件事:“万一充钱的时候突然网卡了,我钱花了但是没有给我点券和皮肤,把我钱吞了怎么办?”。但是这么多年来这个担忧却从来没有发生过,即使是网络质量不佳我们也只会出现以下俩种情况:
- 玩家进行充值后得到了应有的点券或皮肤
- 因为网络卡顿,导致充值失败,玩家既没有花费金钱也没有得到游戏道具
那么在这么一个充皮肤的例子中就有俩个操作:①玩家通过微信支付向游戏厂商进行充值操作 ②游戏厂商将皮肤或者点券发放给玩家。将上述俩种结果的情况转化为逻辑关系则:
- ①成功,并且②也成功
- ①失败,并且②也失败
作为玩家,我们不希望充值后没有得到应有的奖励(①成功,但②失败);作为游戏厂商,我们不希望玩家不消费就获得充值道具(①失败,但②成功)。对于这样充值皮肤的过程,我们就可以将其理解成为一个事务,要么①成功,并且②也成功;要么①失败,并且②也失败,总之俩个操作必须同时成功或者同时失败。
事务的应用遍布在生活中的各处,诸如银行转账、秒杀下单...
作为开发者,通过事务的处理来保证数据的一致性以及用户的体验是必不可少的,在MySQL中我们也常常用到事务的处理
-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
那么在Spring中如何使用事务呢?
二.Spring中事务的实现
Spring中对于事务也进行了实现,Spring中的事务操作分为俩类:
- 编程式事务:手动写代码操作事务
- 声明式事务:利用注解自动开启和提交事务
编程式事务
Spring⼿动操作事务和MySQL 操作事务类似,我们可以通过以下几个操作来完成
- 开启事务(获取事务)
- 提交事务
- 回滚事务
SpringBoot中内置了两个对象用来创建事务:DataSourceTransactionManager 和 TransactionDefinition
- DataSourceTransactionManager 事务管理器,⽤来获取事务(开启事务),提交或回滚事务
- TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从而获得⼀个事务 TransactionStatus
通过依赖诸如的方式将上述俩个对象注入进来后,通过事务管理器对象dataSourceTransactionManager的getTransaction()方法并且将事务属性对象transactionDefinition作为参数传入就可以获取到一个事务对象,即默认此时开启了事务,当我们需要提交或回滚事务的时候就可以通过事务管理器对象的commit()和roolback()方法实现,代码如下:
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;
public String registry(String name,String password){
// 开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
//业务逻辑 用户注册
userService.registryUser(name,password);
//提交事务
dataSourceTransactionManager.commit(transactionStatus);
//回滚事务
//dataSourceTransactionManager.rollback(transactionStatus);
return "注册成功";
}
以上是我们手动设置进行提交和回滚的方式,Spring另外还提供了通过注解来实现事务的方式,即声明式事务
声明式事务
声明式事务的实现很简单,只需要在需要事务的⽅法上添加 @Transactional 注解就可以实现了,⽆需⼿动开启事务和提交事务,进入方法时⾃动开启事务,⽅法执⾏完成会⾃动提交事务,如果中途发⽣了没有处理的异常则会⾃动回滚事务。如下所示:
@Autowired
private UserService userService;
@Transactional
public String registry(String name,String password){
//⽤⼾注册
userService.registryUser(name,password);
return "注册成功";
}
如果抛出了异常,那么整个方法就会自动回滚
@Autowired
private UserService userService;
@Transactional
public String registry(String name,String password){
//⽤⼾注册
userService.registryUser(name,password);
log.info("⽤⼾数据插⼊成功");
//强制程序抛出异常
int a = 10/0;
return "注册成功";
}
@Transactional 可以⽤来修饰⽅法或类,
- 修饰⽅法时:只有修饰 public ⽅法时才⽣效(修饰其他⽅法时不会报错,但也不会⽣效)
- 修饰类时:对 @Transactional 修饰的类中所有的 public ⽅法都⽣效
⽅法/类被 @Transactional 注解修饰时,在⽬标⽅法执⾏开始之前会⾃动开启事务,⽅法执⾏结束之后⾃动提交事务,如果在⽅法执⾏过程中出现异常且异常未被捕获,就进⾏事务回滚操作,如果异常被程序捕获,⽅法就被认为是成功执⾏,依然会提交事务。如下通过try...catch...将异常捕获并弹出异常信息。
@Autowired
private UserService userService;
@Transactional
public String registry(String name,String password){
//⽤⼾注册
userService.registryUser(name,password);
log.info("⽤⼾数据插⼊成功");
//对异常进⾏捕获
try {
//强制程序抛出异常
int a = 10/0;
}catch (Exception e){
e.printStackTrace();
}
return "注册成功";
}
运⾏程序会发现虽然程序出错了,但是由于异常被捕获并且只是弹出了异常信息,并没有通过throw抛出该异常,所以事务依然得到了提交
反之如果想要实现事务的回滚,通过throw抛出异常即可
@Autowired
private UserService userService;
@Transactional
public String registry(String name,String password){
//⽤⼾注册
userService.registryUser(name,password);
log.info("⽤⼾数据插⼊成功");
//对异常进⾏捕获
try {
//强制程序抛出异常
int a = 10/0;
}catch (Exception e){
//将异常重新抛出去
throw e;
}
return "注册成功";
}
另外也可以自己手动设置回滚事务,使⽤ TransactionAspectSupport.currentTransactionStatus() 得到当前的事务,并使⽤ setRollbackOnly 设置 setRollbackOnly
@Autowired
private UserService userService;
@Transactional
public String registry(String name,String password){
//⽤⼾注册
userService.registryUser(name,password);
log.info("⽤⼾数据插⼊成功");
//对异常进⾏捕获
try {
//强制程序抛出异常
int a = 10/0;
}catch (Exception e){
// ⼿动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "注册成功";
}
三.@Transactional 详解
@Transactional 注解中有以下三个常⻅属性:
- rollbackFor: 异常回滚属性,指定能够触发事务回滚的异常类型,可以指定多个异常类型
- Isolation: 事务的隔离级别,默认值为 Isolation.DEFAULT
- propagation: 事务的传播机制,默认值为 Propagation.REQUIRED
▐ 异常回滚属性 rollbackFor
@Transactional默认只在遇到运行时异常和Error时才会回滚,⾮运⾏时异常不回滚。即下图中蓝色部分的内容才会回滚。
如下,我们刻意抛出IOException,但是由于IOException不是运行时异常,也不是Error,则该事务仍然会正常提交事务,并不会进行回滚操作。
@Autowired
private UserService userService;
@Transactional
public String registry(String name,String password) throws IOException {
//⽤⼾注册
userService.registryUser(name,password);
log.info("⽤⼾数据插⼊成功");
if(true) {
throw new IOException();
}
return "注册成功";
}
如果我们希望其他的异常也会引起事务的回滚,那么通过rollbackFor属性来设置即可,如下就是对于所有异常的抛出都会引起事务的回滚。一旦我们按照下面这样编码,那么即使是抛出IO异常也会引起事务的回滚,因为IOException是继承于Exception的子类。
@Autowired
private UserService userService;
@Transactional(rollbackFor = Exception.class)
public String registry(String name,String password) throws IOException {
//⽤⼾注册
userService.registryUser(name,password);
log.info("⽤⼾数据插⼊成功");
if(true) {
throw new IOException();
}
return "注册成功";
}
▐ 事务隔离级别 Isolation
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题
首先回顾一下SQL中的事务隔离级别,SQL 标准定义了四种隔离级别,且MySQL 全都⽀持,这四种隔离级别分别是:
1.读未提交(READ UNCOMMITTED):读未提交,也叫未提交读。该隔离级别的事务可以看到其他事务中未提交的数据。
隐患:因为其他事务未提交的数据可能会发⽣回滚,但是该隔离级别却可以读到,我们把该级别读到的数据称之为脏数据,这个问题称之为脏读
2.读提交(READ COMMITTED):读已提交,也叫提交读。该隔离级别的事务能读取到已经提交事务的数据。
隐患:该隔离级别不会有脏读的问题,但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询可能会得到不同的结果,这种现象叫做不可重复读
3.可重复读(REPEATABLE READ):事务不会读到其他事务对已有数据的修改,即使其他事务已提交,也可以确保同⼀事务多次查询的结果⼀致,并且可重复读是 MySQL 的默认事务隔离级别。
隐患:由于其他事务新插⼊的数据也是可以感知到的,这也就引发了幻读问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因),明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这个现象叫幻读
4.串行化(SERIALIZABLE):序列化,事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突, 从而解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多
Spring中事务隔离级别有5种,相较于上述的SQL的事务隔离级别,多了一个DEFAULT级别,该级别表示当前连接的数据库是什么就用该数据库的事务隔离级别;比如我们连上MySQL,由于MySQL的默认级别是可重复读,那么在我们不做任何修改的情况下,Spring中的事务隔离级别就是可重复读。
- Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主
- Isolation.READ_UNCOMMITTED:读未提交,对应SQL标准中 READ UNCOMMITTED
- Isolation.READ_COMMITTED:读已提交,对应SQL标准中 READ COMMITTED
- Isolation.REPEATABLE_READ:可重复读,对应SQL标准中 REPEATABLE READ
- Isolation.SERIALIZABLE:串⾏化,对应SQL标准中 SERIALIZABLE
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置
@Transactional(isolation = Isolation.READ_COMMITTED)
▐ 事务的传播机制 propagation
事务传播机制:多个事务⽅法存在调⽤关系时,控制事务如何在这些⽅法间进⾏传播的机制
⽐如有两个⽅法A、B都被 @Transactional 修饰,且A⽅法调⽤B⽅法。A⽅法运⾏时会开启⼀个事务,当A调⽤B时,B⽅法本⾝也有事务,当B⽅法运⾏时是加⼊A的事务,还是创建⼀个新的事务呢?
这就涉及到了事务的传播机制,事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题,而事务传播机制解决的是⼀个事务在多个⽅法中传递的问题
@Transactional 注解⽀持事务传播机制的设置,通过 propagation 属性来指定传播⾏为
Spring 事务传播机制有以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别。如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。
- Propagation.MANDATORY:如果当前存在事务,则加⼊该事务;如果当前没有事务, 则抛出异常(强制性)
- Propagation.REQUIRES_NEW:不管当前有没有事务都要创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。
- Propagation.NOT_SUPPORTED:以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起(不⽤)
- Propagation.NEVER:以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
- Propagation.NESTED : 如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
上文中的描述可能有些枯燥,我们可以用一对新人买房的实况来理解事务传播机制:
- Propagation.REQUIRED : 需要有房⼦,如果你有房,我们就⼀起住;如果你没房,我们就⼀起买房。(如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则创建⼀个新的事务)
- Propagation.SUPPORTS : 可以有房⼦,如果你有房,那就⼀起住;如果没房,那就租房. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以⾮事务的⽅式继续运⾏)
- Propagation.MANDATORY : 如果你有房,我们就⼀起住;如果没房就离婚 (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则抛出异常)
- Propagation.REQUIRES_NEW : 必须买新房,不管你有没有房,必须要两个⼈⼀起买房,即使有房也不住。 (创建⼀个新的事务. 如果当前存在事务, 则把当前事务挂起)
- Propagation.NOT_SUPPORTED : 不需要房,不管你有没有房,我都不住,必须租房。(以⾮事务⽅式运⾏,如果当前存在事务, 则把当前事务挂起)
- Propagation.NEVER : 不能有房⼦。 (以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常)
- Propagation.NESTED : 如果你没房,就⼀起买房;如果你有房,就在这个房子里面再修一个养宠物的小房子。(如果如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏. 如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED )
本次的分享就到此为止了,希望我的分享能给您带来帮助,创作不易也欢迎大家三连支持,你们的点赞就是博主更新最大的动力!
如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步!
有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见