事务基础知识及Spring框架中使用Transactioal踩得坑

0.为什么写这篇文章

​ 某天,app端开发人员突然在钉钉上提了个问题单描述为:在app端创建群组功能时,明明提示系统错误但是在查询列表中却能看到刚刚创建的群组数据,但是点击进去却有提示错误异常。
收到这个bug单后,直接快速过了一遍前人写的代码发现里面有涉及到三个表的数据新增操作,但是整个业务的执行步骤中却没有追加事务操作,导致了前面的数据入库了后面的业务逻辑却出现了问题,但是之前的入库数据却没有回滚。

1.什么是事务

​ 用作者本人的话来理解就是:由一段组合sql组成的业务逻辑sql块,要么全部成功、要么全部不成功,一个不可分割的单位。

事务的四大特性:

  • 原子性(Atomicity):事务是数据库的逻辑工作单位,事务中包含的操作要么都执行,要么都不执行。
  • 隔离性(lsolation):一个事务的执行不受其它事务的干扰,即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能相互干扰。
  • 持久性(Durability):成功提交后的后事务数据是持久有效的。
  • 一致性(Consistency):事务的提交前后状态是一致的,要么都成功,要么都失败,不可能成功一半失败半。

事务的实现主要分为:编程式事务和声明式事务两种

  • 编程式事务:是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强且写起来繁琐,

代码如下:

1
2
3
4
5
6
7
8
9
Session session = getSessionFactory().openSession();
session.beginTransaction(); // 开启事务
Criteria criteria = session.createCriteria(SysWarning.class);
criteria.add(Restrictions.eq("id", 1));
SysWarning sysWarning = (SysWarning) criteria.uniqueResult();
sysWarning.setIsDelete(0);
session.saveOrUpdate(sysWarning);
session.getTransaction().commit();// 提交事务
session.close();
  • 声明式事务:基于aop面向切面实现,它将具体业务与事务处理部分解耦,代码侵入性很低,所以在实际开发中声明式事务用的比较多。声明式事务有两种实现方式,一种是基于TX和AOP的XML配置方式实现,一种是基于@Transactional注解实现。

1.基于XT和AOP的xml方式实现(这种方式一般出现在SpringMVC项目中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 拦截器方式配置事物 -->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="add*" />
<tx:method name="save*" />
<tx:method name="update*" />
<tx:method name="modify*" />
<tx:method name="edit*" />
<tx:method name="delete*" />
<tx:method name="remove*" />
<tx:method name="repair" />
<tx:method name="deleteAndRepair" />
<tx:method name="get*" propagation="SUPPORTS" />
<tx:method name="find*" propagation="SUPPORTS" />
<tx:method name="load*" propagation="SUPPORTS" />
<tx:method name="search*" propagation="SUPPORTS" />
<tx:method name="datagrid*" propagation="SUPPORTS" />

<tx:method name="*" propagation="SUPPORTS" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* com.xx.xx.service..*Impl.*(..))" />
<aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
</aop:config>
</beans>

2.基于@Transactional注解

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public ResultJson commentDelete(int[] id) {
List<Integer> ids = Arrays.stream(id).boxed().collect(Collectors.toList());
try {
commentDaoI.deleteByAid(ids);
commentDaoI.deleteByFloorId(ids);
} catch (Exception e) {
logger.error("delete error msg:[{}]", e.getMessage());
throw new Exception("xxx");
}
}

2.@Transactional介绍

1、@Transactional注解可以作用于那些地方?

@Transactional可以作用在接口、类、类方法

  • 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息。
  • 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
  • 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效

2、@Transactional注解有哪些属性7个?

propagetion属性

  • propagation 代表事务的传播行为,默认值为 Propagation.REQUIRED,其他的属性信息如下:
  • Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )
  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )
  • Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。Propagation.NESTED :和 Propagation.REQUIRED 效果一样。

isolation属性

isolation :事务的隔离级别,默认值为

Isolation.DEFAULT

solation.DEFAULT:使用底层数据库默认的隔离级别。

Isolation.READ_UNCOMMITTED(读未提交),该隔离级别的事务会读到其它未提交事务的数据,此现象称之为脏读。

Isolation.READ_COMMITTED(读已提交),一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象成为不可重复读问题,Oracle和SQL Server的默认隔离级别

Isolation.REPEATABLE_READ(可重复读),该隔离级别是Mysql默认的隔离级别,在同一个事务里,select的结果是事务开始时时间点的状态,因此,同样的select操作读到的结果会是一致的,但是会有幻读的现象,MYsql的InnoDB引擎可以通过next-key-locks机制来避免幻读。

Isolation.SERIALIZABLE(串行化)在该隔离级别下事务都是串行顺序执行的,mysql数据库饿innoDB引擎会给读操作隐式加一把共享锁,从而避免了脏读、不可重复度和幻读问题。

timeout属性

timeout :事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务

readOnly属性

readOnly :指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

rollbackFor属性

rollbackFor :用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。

noRollbackFor属性

noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

3.@Transacationl失效场景

1、@Transactional 应用在非 public 修饰的方法上

如果Transactional注解应用在非public修饰的方法上,Transactional将会失效。

之所以会失效是因为在Spring AOP 代理时, TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute

方法,获取Transactional 注解的事务配置信息,此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

注意:protected、private修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

2、@Transactional 注解属性 propagation 设置错误

这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

3、@Transactional 注解属性 rollbackFor 设置错误

rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。

若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。

4、同一个类中方法调用,导致@Transactional失效

开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。

那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

5、异常被你的 catch“吃了”导致@Transactional失效

这种情况是最常见的一种@Transactional注解失效场景,

如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务不能正常回滚。

错误的实例:因为异常被catch捕获且处理了。

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional
public ResultJson commentDelete(int[] id) {
List<Integer> ids = Arrays.stream(id).boxed().collect(Collectors.toList());
try {
commentDaoI.deleteByAid(ids);
commentDaoI.deleteByFloorId(ids);
} catch (Exception e) {
logger.error("delete error msg:[{}]", e.getMessage());
return new ResultJson().setErrorCode(ExceptionEnum.SYSTEM_ERROR.getErrorCode()).setMessage(ExceptionEnum.SYSTEM_ERROR.getMessage());
}
return new ResultJson().setMessage("ok");
}

6、数据库引擎不支持事务

这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了

4.分布式事务