一.事务的特性
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序 执行逻辑单元(Unit)。事务具有四个特征,分别是原子性(Atomicity )、一致性(Consistency )、隔离性(Isolation) 和持久性(Durability),简称为事务的ACID特性。
- 原子性(Atomicity):原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency):事务执行前后数据的完整性必须保持一致。比如在转账事务操作中,事务执行前后金额的总数应保持不变。
- 隔离性(Isolation):事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
- 持久性(Durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
二.事务并发执行带来的问题
1.丢失修改(丢失更新):两个事务T1和T2读入同一数据并修改,T2的提交结果破坏了T1提交的结果,导致T1的修改被丢失。
2.不可重复读:事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读该数据时,得到与前一次不同的值。
3.幻读:事务T1按一定条件从数据库中读取了某些数据记录后,事务T2删除了其中部分记录,当T1再次按相同条件读取数据时,发现某些记录消失了;或者说事务T2插入了部分新记录,当T1再次按相同条件读取数据时,发现多出来了部分数据。
注意:幻读和不可重复读的主要区别在于:
- 幻读针对的是查询结果为多个的场景,出现了数据的增加 or 减少
- 不可重复度读对的是某些特定的记录,这些记录的数据与之前不一致
4.读“脏”数据:事务T1修改某一数据后,事务T2读取同一数据,然后T1由于某种原因操作被撤销(回滚),这时T1已修改过的数据恢复原值,T2读到的数据就与数据库中的真实数据不一致,这时T2读到的数据就为“脏”数据,即不正确的数据。
三.SpringBoot中的事务控制
- Spring提供了一个@EnableTransactionManagement注解在配置类上来开启声明式事务的支持(开启注解支持)。使用了@EnableTransactionManagement后,Spring容器会自动扫描注解@Transactional的方法和类。SpringBoot默认已开启,可不用显式注解。
- 要使用事务,我们只需要在需要事务的类或方法上使用@Transactional 注解即可,当注解在类上的时候意味着此类的所有public方法都是开启事务的。被注解的方法都成为一个事务整体,同一个事务内共享一个数据库连接,所有操作同时发生。如果在事务内部执行过程中发生了异常,则事务整体会自动进行回滚。
1.事务隔离级别
事务之间有不同的隔离级别,不同的隔离级别可以解决并发事务所带来的问题。在SpringBoot中,事务的隔离级别定义为@Transactional 注解中的属性@Transactional(isolation = Isolation.DEFAULT)
int ISOLATION_DEFAULT = -1;//默认的隔离级别,使用当前数据库的隔离级别。会是后面四种隔离级别的其中一种
int ISOLATION_READ_UNCOMMITTED = 1;//读未提交;
int ISOLATION_READ_COMMITTED = 2; //读已提交;
int ISOLATION_REPEATABLE_READ = 4; //重复读;
int ISOLATION_SERIALIZABLE = 8; //串行化
DEFAULT
:默认值(也是SpringBoot的隔离级别默认值),表示使用底层数据库的默认隔离级别。大部分数据库为READ_COMMITTED(MySql默认隔离级别为REPEATABLE_READ)
READ_UNCOMMITTED
:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。 (解决了丢失修改,但会出现脏读,不可重复读和幻读)READ_COMMITTED
:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。 (解决了更新丢失,脏读。但会出现不可重复读,幻读)REPEATABLE_READ
:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。 (解决了更新丢失,脏读,不可重复度,同时有人指出在 mysql 的 innodb 引擎上,配合 mvvc + gap 锁,已经解决了幻读问题)SERIALIZABLE
:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 (解决所有问题,串行执行)
2.事务传播方式
在使用事务时,如果一个事务方法调用另一个事务方法,那么不同事务之间的执行应该如何调度呢?这就是事务传播的作用,用来调度不同事务执行时的顺序和方式。在SpringBoot中,事务的隔离级别定义为@Transactional 注解中的属性@Transactional(propagation = Propagation.REQUIRED)
int PROPAGATION_REQUIRED = 0;//如果当前上下文中的已经存在一个事务,就使用当前的事务;如果当前没有事务就创建一个新的事务
int PROPAGATION_SUPPORTS = 1;//如果当前上下文中的已经存在一个事务,就使用当前的事务;如果当前没有事务也不会开启一个新事务,以非事务的方式执行
int PROPAGATION_MANDATORY = 2;//如果当前上下文中的已经存在一个事务,就使用当前的事务;如果当前没有事务就抛出异常
int PROPAGATION_REQUIRES_NEW = 3;//如果当前上下文中的已经存在一个事务,就暂停该事务;创建一个新的事务开始执行。完成后恢复之前挂起的事务
int PROPAGATION_NOT_SUPPORTED = 4;//不支持事务,如果当前上下文中已经存在一个事务,就挂起事务;已非事务的方式执行
int PROPAGATION_NEVER = 5;//不支持事务,如果当前上下文中已经存在一个事务,就抛出异常
int PROPAGATION_NESTED = 6;//如果当前上下文中已经存在事务,就开启一个嵌套的事务。简单来说就是不管当前上下文中是否存在事务,本次都会创建一个事务
- PROPAGATION_REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。也就是说业务方法需要在一个事务中运行,如果业务方法被调用时,调用业务方法的行为(方法)已经处在一个事务中,那么就加入到该事务,否则为自己创建一个新的事务。(默认传播属性)
- PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。也就是说如果业务方法在某个事务范围内被调用,则该方法成为该事务的一部分。如果业务方法在事务范围外被调用,则该方法在没有事务的环境下执行。
- PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。也就是说业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果业务方法在没有事务的环境下被调用,容器就会抛出例外。
- PROPAGATION_REQUIRESNEW:新建事务,如果当前存在事务,把当前事务挂起。也就是说业务方法被调用时,不管是否已经存在事务,业务方法总会为自己发起一个新的事务。如果调用业务方法的行为(方法)已经运行在一个事务中,则原有事务会被挂起,新的事务会被创建,直到业务方法执行结束,新事务才算结束,原先的事务才会恢复执行。
- PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,就把当前事务挂起。也就是说业务方法不需要事务。如果方法没有被关联到一个事务中,容器不会为它开启事务。如果方法在一个事务中被调用,该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行。
- PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。也就是说业务方法绝对不能在事务范围内执行。如果业务方法在某个事务中执行,容器会抛出例外,只有业务方法没有关联到任何事务,才能正常执行。
- PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按REQUIRED属性执行。它使用了一个单独的事务, 这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。
3.事务其他属性
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
Propagation propagation() default Propagation.REQUIRED;//默认的传播行为
Isolation isolation() default Isolation.DEFAULT;//默认的隔离级别
int timeout() default -1;//-1表示不过期
String timeoutString() default "";
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
四.SpringBoot中事务控制失效的原因
- 检查你方法是不是public的(只有Public方法才能开启事务)
- 检查你的异常类型是不是unchecked异常(抛出的是unchecked类型(RuntimeException级别)的异常,默认事务回滚不会生效。)
- 检查数据库引擎是否支持事务,如果是MySQL,注意表要使用支持事务的引擎,比如innodb,如果是myisam,事务是不起作用的
- 检查异常是不是被catch住了(当异常被捕获后,并且没有再抛出,那么事务是不会回滚的。)
- 检查是否在事务中新开启了一个线程(因为spring实现事务的原理是通过ThreadLocal把数据库连接绑定到当前线程中,新开启一个线程获取到的连接就不是同一个了。)
- 检查类有没有被Spring管理(方法被标注了@Transactional,但是类没有注解,没有被Spring管理也不会生效)
- 类内部未开启事务的方法调用了开启事务的方法
五.SpringBoot事务注解原理分析
@Transactional事务注解由SpringBoot控制。只有@Cacheable缓存注解失效,执行具体逻辑时才会激活事务控制。事务注解的原理是AOP,对相关注解方法进行动态代理,实现动态增强,其本质是在逻辑执行前后进行反射动态加上事务控制的手动代码逻辑。
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
.............
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 开启事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
//递归执行拦截链
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 出现异常,回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
//提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
.................
}
注意:
mysql默认隔离级别:可重复读 。在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。也就是某个事务(在SpringBoot中表现为加了@Transactional注解的方法为一个事务整体)在执行的过程中,不允许其他事务进行相关字段的update操作(加行锁),但允许其他事务进行add操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读。这是mysql的默认事务隔离级别。
异常回滚类型:注解 @Transactional只对unchecked异常进行事务回滚,可以通过添加@Transactional(rollbackFor=Exception.class) 来对所有异常进行回滚。