Spring事务的隔离级别与传播行为
什么是事务
是数据操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么全部执行,要么都不执行(也就是说要么全部成功,要么全部失败);事务是一组不可再分割的操作集合(工作逻辑单元)。
常见的例子就是转账,例如A给B转账1000元
开启事务 -> A账户扣除1000 -> B账户增加1000 -> 提交事务
上面的事务任何一个步骤出错都会导致事务回滚
事务的四大特性(ACID)
-
原子性(Atomicity):一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。例如转账,要么成功,要么失败,不存在中间状态。
利用InnoDB的undo log(回滚日志),它是实现原子性的关键,事务回滚时能够撤销所有已经执行成功的sql语句,它需要记录要回滚的相应日志信息。 -
隔离性(Isolation):多个事务并发执行的时候,事务内部的操作与其它事务是隔离的,并发执行的各个事务之间不能互相干扰。
例:A账户有200元,分成两个事务向B转账两次50元,并发执行,若前一个事务还未把50元写回磁盘就被后一个事务读取到,两个事务读取到的B的余额都0元,最终B一共就只增加了50元。
锁和MVCC -
持久性(Durability):事务一旦提交它对数据库的改变是永久性的,接下来的任何操作或故障对其不会有任何影响。
利用InnnoDB的redo log,当有数据在进行修改时,不仅会在内存中进行操作,还会再redo log中记录这次操作。事务提交时,会将redo log进行刷盘。数据库宕机重启时,会将redo log中的内容恢复到数据库中。 -
一致性(Consistency):事务的执行结果必须是使数据库从一个一致性状态变到另一个一致性状态(事务执行前后,数据处于一种合法的状态。也就是说满足自己预定的约束,这个数据就是合法的)。
例:A向B转账50元,A扣除了50元,可是B由于种种原因没有收到这50元,此时这50元凭空消失了。这显然是不合法的,因为自己预定了约束:不论转账成功与否,参与转账的两个账户余额总和是不变的。
显然,C是目的,AID都是为了达到C的手段,数据库必须满足AID三大特性,才有可能实现一致性;由于自身的原因也是无法达到一致性的,例如A扣了钱,故意不给B加钱。
JDBC事务处理
try{
// 开启事务
connection.setAutoCommit(false);
// 一些sql操作 insert update delete...
// 提交事务
connection.commit();
} catch(Exception e) {
// 异常回滚事务
connection.rollback();
}
事务并发带来的问题
- 脏读:事务A读取到事务B未提交的数据;
- 不可重复读:一个事务中对某个数据读取了两次,两次读取到的结果不一致;
- 幻读:一个事务中,同一查询条件下,第二次查询出现了第一次查询所没有的数据;
Spring事务隔离级别
事务的隔离级别也就是为了解决事务并发带来的各种问题,以下是Spring提供的事务隔离级别枚举类,可以看到Spring提供了五种事务隔离级别:
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
- DEFAULT(默认):使用数据库默认的隔离级别,InnoDB存储引擎默认为RR,即可重复读;
- READ_UNCOMMITTED(读未提交):事务最低的隔离级别,能够读取到没有被提交的数据,这种隔离级别会产生脏读、不可重复读、幻读;(读不加锁,写会加行级共享锁)
- READ_COMMITTED(读已提交):A事务能够读取到B事务提交之后的数据。能够避免脏读的出现,但是会产生不可重复读、幻读;(写加行级排他锁,事务结束才会释放,读加行级共享锁,读到数据便会释放,如A事务读到数据之后释放锁,B事务则可以修改该数据,A再读的时候数据就不一样了,则会产生不可重复读)。
- REPEATABLE_READ(可重复读):可以重复读取,一个事务中对同一数据的读取结果是不变的。能够避免脏读、不可重复读,但是会产生幻读;(读取数据时加行级共享锁、行级排他锁,事务结束才会释放,无法控制插入数据,因此会产生幻读)
- SERIALIZABLE(串行化):最高的事务隔离级别,完全串化执行,毫无并发可言,性能极低,采用了表级共享锁与排他锁(事务结束释放),能够避免脏读、不可重复读、幻读。
那么隔离级别越高越好咯?答案肯定是否定的,隔离级别越高,效率也就越低。
Spring事务的传播行为
单独一个事务不可以传播,所以,事务的传播行为是当一个事务方法被另外一个事务方法调用时,这个事务方法应该如何进行,以下是Spring事务传播行为的枚举类,这里可以看到,Spring定义了七种事务的传播行为。
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
}
- REQUIRED:如果当前存在事务,则加入到这个事务,如果当前没有事务,则新建一个事务,这个是Spring默认的传播行为。
如果当前存在事务A,此时调用事务B,B会和A一起提交,无论是A还是B中发生的异常都会引起A、B的操作一起回滚。
package com.mezjh.blog.spring.transaction.propagationbehavior;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
/**
* Spring事务传播行为一:REQUIRED 若当前存在事务,则加入到该事务,若不存在事务,则新建一个事务
* 由于@Transactional注解默认的传播行为就是REQUIRED,所以在这里未注明传播行为
* @author ZJH
* @date 2021/7/6 16:44
*/
@Service
public class Pb1RequiredA {
@Resource
private Pb1RequiredB pb1RequiredB;
@Resource
private PbUserMapper pbUserMapper;
/**
* 若当前不存在事务 则会新建一个事务
* 结论:
* 1.A1非事务方法,B1是事务方法,A1调用B1,B1会新建一个事务
* 2.无论是在A1还是在B1中发生异常,A1的操作都不会回滚。B1的回滚仅发生在B1发生异常时。
*/
public void methodA1() {
List<PbUser> allUser = pbUserMapper.getAllUser();
allUser.forEach(x -> pbUserMapper.workAdd(x.getId()));
pb1RequiredB.methodB1();
throw new RuntimeException();
}
/**
* 若当前存在事务,则加入到该事务中
* 结论:
* 1.A2和B1都是事务方法,A2中调用B1,B1会加入到A2的事务中,并和A2的事务一起提交
* 2.无论是在A2还是在B1中发生异常,A2和B1的操作都会被回滚
*/
@Transactional(rollbackFor = Exception.class)
public void methodA2() {
List<PbUser> allUser = pbUserMapper.getAllUser();
pbUserMapper.id1Add();
try {
pb1RequiredB.methodB1();
} catch (Exception e) {
}
List<PbUser> allUser2 = pbUserMapper.getAllUser();
throw new RuntimeException();
}
@Service
public class Pb1RequiredB {
@Resource
private PbUserMapper pbUserMapper;
@Transactional(rollbackFor = Exception.class)
public void methodB1() {
List<PbUser> allUser = pbUserMapper.getAllUser();
pbUserMapper.id2Add();
List<PbUser> allUser2 = pbUserMapper.getAllUser();
// throw new RuntimeException();
}
}
}
- SUPPORTS:支持当前事务,如果当前没有事务,则以非事务的方式运行。
如果当前存在事务A,调用事务B,则B会加入到事务A一起提交,无论是A还是B中的异常都会引起A、B的操作一起回滚。
package com.mezjh.blog.spring.transaction.propagationbehavior;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
/**
* Spring事务传播行为二:Supports 支持当前事务,若当前没有事务,则以非事务的方式运行
* @author ZJH
* @date 2021/7/6 17:54
*/
@Service
public class Pb2SupportsA {
@Resource
private PbUserMapper pbUserMapper;
@Resource
private Pb2SupportsB pb2SupportsB;
/**
* 若当前没有事务,则以非事务的方法运行
* 结论:
* 1.A1非事务方法,B1以非事务方法运行
* 2.无论在A1还是B1中发生异常,A1和B1的操作都不会回滚
*/
public void methodA1() {
List<PbUser> allUser = pbUserMapper.getAllUser();
allUser.forEach(x -> pbUserMapper.workAdd(x.getId()));
pb2SupportsB.methodB1();
// throw new RuntimeException();
}
/**
* 若当前存在事务,则支持该事务
* 结论:
* 1.A2和B1都是事务方法,A2中调用B1,B1加入到A2并一起提交
* 2.无论在A2还是B1中发生异常,A2和B1都会被回滚
*/
@Transactional(rollbackFor = Exception.class)
public void methodA2() {
List<PbUser> allUser = pbUserMapper.getAllUser();
pbUserMapper.id1Add();
try {
pb2SupportsB.methodB1();
} catch (Exception e) {
}
List<PbUser> allUser2 = pbUserMapper.getAllUser();
throw new RuntimeException();
}
@Service
public class Pb2SupportsB {
@Resource
private PbUserMapper pbUserMapper;
@Transactional(rollbackFor = Exception.class, propagation = Propagation.SUPPORTS)
public void methodB1() {
List<PbUser> allUser = pbUserMapper.getAllUser();
pbUserMapper.id2Add();
List<PbUser> allUser2 = pbUserMapper.getAllUser();
// throw new RuntimeException();
}
}
}
- MANDATORY:支持当前事务,如果当前没有事务,则抛出异常。
此处不再以代码说明,若当前有事务,则加入到当前事务,随着当前事务一起提交,发生异常两个事务的操作都会被回滚。若当前没有事务,则会抛出异常。调用事务之前的所有操作无法回滚。
- REQUIRES_NEW:新建事务,如果当前存在事务,则把当前事务挂起。
不管当前有没有事务都会新建一个事务,若当前存在事务A,则A会被挂起,B事务提交后才会继续执行A事务。A、B中的异常都只会影响自身的操作回滚,彼此不受影响。代码如下:
package com.mezjh.blog.spring.transaction.propagationbehavior;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
/**
* Spring事务传播行为四:RequiresNew 不管当前有没有事务都会新建一个事务,若当前存在事务,则会把当前事务挂起
* @author ZJH
* @date 2021/7/6 17:54
*/
@Service
public class Pb4RequiresNewA {
@Resource
private PbUserMapper pbUserMapper;
@Resource
private Pb4RequiresNewB pb4RequiresNewB;
/**
* 若当前存在事务,则会把当前事务挂起,并开启一个新事务
* 结论:
* 1.事务方法A1调用事务方法B1时,A1会被挂起,B1提交后才会继续执行A1
* 2.A1中的异常不会影响B1中的操作回滚,B1中的异常也不会影响A1中的操作回滚,它们都只会影响自身的操作回滚
* 3.这里需要注意的是,对数据的修改会对这行数据增加排他锁;若这里在A1有修改,未提交,此时B1新建一个事务
* 也去修改相同数据,会获取锁超时。
*/
@Transactional(rollbackFor = Exception.class)
public void methodA1() {
List<PbUser> allUser = pbUserMapper.getAllUser();
pbUserMapper.id1Add();
try {
pb4RequiresNewB.methodB1();
} catch (Exception e) {
}
List<PbUser> allUser2 = pbUserMapper.getAllUser();
throw new RuntimeException();
}
@Service
public class Pb4RequiresNewB {
@Resource
private PbUserMapper pbUserMapper;
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void methodB1() {
List<PbUser> allUser = pbUserMapper.getAllUser();
pbUserMapper.id2Add();
List<PbUser> allUser2 = pbUserMapper.getAllUser();
// throw new RuntimeException();
}
}
}
- NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,则把当前事务挂起。
此处不再以代码说明,若当前存在事务,当前事务会被挂起,以非事务的方式运行,此时在事务A中调用事务B(传播行为是NOT_SUPPORTED),A会被挂起,B以非事务的方式执行,B中发生的异常不会使A、B中的操作回滚,A中发生异常只会使A的操作回滚。
-
NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
与NOT_SUPPORTED不同的是,若当前存在事务,会抛出异常。
-
NESTED:如果当前存在事务,则在嵌套事务内进行。如果当前没有事务,则与REQUIRED类似的操作。
若事务方法A调用事务方法B,则B嵌套执行,与A一起提交,B中的异常只会引起B的操作回滚,A中的异常会让A、B中的操作一起回滚
package com.mezjh.blog.spring.transaction.propagationbehavior;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
/**
* Spring事务传播行为七:Nested 若当前存在事务,则在嵌套事务内执行,若当前没有事务,则按照Required的方式执行
* @author ZJH
* @date 2021/7/6 17:56
*/
@Service
public class Pb7NestedA {
@Resource
private PbUserMapper pbUserMapper;
@Resource
private Pb7NestedB pb7NestedB;
/**
* 若当前存在事务,则嵌套执行,若当前没有事务,则同Required
* 结论:
* 1.A1与B1都是事务方法,B1嵌套A1执行
* 2.B1中的异常只会引起B1中的操作回滚,A1中的异常会让A1、B1的操作都回滚
*/
@Transactional(rollbackFor = Exception.class)
public void methodA1() {
List<PbUser> allUser = pbUserMapper.getAllUser();
pbUserMapper.id1Add();
// allUser.forEach(x -> pbUserMapper.workAdd(x.getId()));
try {
pb7NestedB.methodB1();
} catch (Exception e) {
}
List<PbUser> allUser2 = pbUserMapper.getAllUser();
// allUser2.forEach(x -> pbUserMapper.workAdd(x.getId()));
// throw new RuntimeException();
}
@Service
public class Pb7NestedB {
@Resource
private PbUserMapper pbUserMapper;
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
public void methodB1() {
List<PbUser> allUser = pbUserMapper.getAllUser();
pbUserMapper.id2Add();
// allUser.forEach(x -> pbUserMapper.ageAdd(x.getId()));
List<PbUser> allUser2 = pbUserMapper.getAllUser();
throw new RuntimeException();
}
}
}