Spring事务入门简介及AOP陷阱分析
转载请注明出处: https://www.cnblogs.com/qnlcy/p/15237377.html
一、事务的定义
事务(Transaction),是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit),是恢复和并发控制的基本单位。
事务的产生,其实是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.
二、事务的属性
事务具有4个属性,简称 ACID
属性 | 说明 | |
---|---|---|
Atomicity | 原子性 | 一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。 |
Consistency | 一致性 | 事务执行的结果必须是使数据库从一个一致性状态c0变到另一个一致性状态c1 |
Isolation | 隔离性 | 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 |
Durability | 持久性 | 指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。 |
三、Spring 事务的隔离级别
当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。
在介绍数据库提供的各种隔离级别之前,我们先看看如果不考虑事务的隔离性,会发生的几种问题
3.1 隔离级别引出的问题
3.1.1 脏读
是指在没有隔离的情况下,一个事务读取了另外一个事务已修改但未提交(有可能回滚也有可能继续修改)的缓冲区数据。
3.1.2 不可重复读
数据库中的某项数据在一个事务多次读取,但是在多次读取期间,其他事务对其有修改并提交,导致返回值不同,这就发生了不可重复读。
不可重复读侧重修改。
3.1.3 幻读
幻读和不可重复读相似。当一个事务(T1)读取几行记录后(事务并没有结束),另一个并发事务(T2)插入了一些记录时,幻读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。
幻读侧重新增或者删除。
3.2 隔离级别
在理想状态下,事务之间将完全隔离(即下表中的 Isolation.SERIALIZABLE
),从而可以防止这些问题发生。
然而,完全隔离会影响性能,因为隔离经常涉及到锁定在数据库中的记录(甚至有时是锁表)。
完全隔离要求事务相互等待来完成工作,会阻碍并发。因此,可以根据业务场景选择不同的隔离级别。
隔离级别 | 含义 |
---|---|
Isolation.DEFAULT | 使用后端数据库默认的隔离级别 |
Isolation.READ_UNCOMMITTED | 允许读取尚未提交的更改。可能导致脏读、幻读或不可重复读。 |
Isolation.READ_COMMITTED | (Oracle 默认级别)允许从已经提交的并发事务读取。可防止脏读,但幻读和不可重复读仍可能会发生。 |
Isolation.REPEATABLE_READ | (MYSQL默认级别)对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻读仍可能发生。 |
Isolation.SERIALIZABLE | 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。 |
四、Spring 事务的传播机制
Spring 事务的传播机制描述了在嵌套事务当中,当前事务与外部事务(最近的那个,有可能没有)的继承关系。
比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。
Spring 事务的传播有如下机制
类型 | 描述 |
---|---|
PROPAGATION_REQUIRED | Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行 |
PROPAGATION_REQUES_NEW | 该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可 |
PROPAGATION_SUPPORT | 如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务 |
PROPAGATION_NOT_SUPPORT | 该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码 |
PROPAGATION_NEVER | 该传播机制不支持外层事务,即如果外层有事务就抛出异常 |
PROPAGATION_MANDATORY | 与NEVER相反,如果外层没有事务,则抛出异常 |
PROPAGATION_NESTED | 该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。 |
五、Spring 事务的应用(声明式)
Spring 声明式事务是指依托注解 @Transactional
和 AOP
功能,在其方法两端添加事务的操作,实现对被注解修饰方法的增强
。
5.1 事务只读
从事务开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)。
事务只读只适用于 当传播机制为
PROPAGATION_REQUIRED
,PROPAGATION_REQUES_NEW
的情况
5.1.1 应用场景
在诸如统计查询、报表查询的过程当中,需要多次查询,为了避免在查询过程当中对剩余查询数据的修改,保证数据整体在某一时刻的一致性,需要使用只读事务。
5.1.2 使用方式
@Transactional(propagation = Propagation.REQUIRES, readOnly = true)
public List<Product> findAllProducts() {
return this.productDao.findAllProducts();
}
5.2 事务回滚
在事务注解 @Transactional
中指定了某个异常后,捕获到事务方法抛出了该异常或者其子类异常,会造成事务回滚。默认当捕获到方法抛出的 RuntimeException
异常后,事务就会回滚。还可以设置当出现某异常时候不回滚,即使是运行时异常
5.2.1 使用方式
// 回滚Exception类型异常
@Transactional(rollbackFor = Exception.class)
public void test1() throws Exception {
// ..
}
// 回滚自定义类型异常
@Transactional(rollbackForClassName = "org.transaction.demo.CustomException")
public void test2() throws Exception {
// ..
}
// 不回滚自定义类型异常
@Transactional(noRollbackFor = CustomException.class)
public void test3() throws Exception {
// ..
}
5.3 事务超时
如果一个事务长时间占用数据库连接,会导致服务等待从而引起服务雪崩效应,所以设置一个合理的超时时间,是必要的。默认不超时。事务超时会引起事务回滚。
事务超时只适用于 当传播机制为
PROPAGATION_REQUIRED
,PROPAGATION_REQUES_NEW
的情况
5.3.1 使用方式
//设置事务超时时间,单位秒
@Transactional(timeout = 5)
public void test() {
// ..
}
5.4 事务传播机制的使用方式
//每次外层事务调用都会开启一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test() {
// ..
}
5.5 事务隔离机制的使用方式
指定事务隔离机制只适用于 当传播机制为
PROPAGATION_REQUIRED
,PROPAGATION_REQUES_NEW
的情况
//设置事务隔离级别为串行
@Transactional(isolation = Isolation.SERIALIZABLE))
public void test() {
// ..
}
六、Spring 声明式事务的 AOP 陷阱
总所周知,声明式事务依托 AOP
功能实现对事务方法的增强,而 AOP
底层则是代理,存在代理陷阱。
6.1 AOP 代理陷阱复现
@Transactional(rollbackFor = RuntimeException.class)
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException("");
}
/**
* 内部调用新增方法
*/
public void insertMale(User user) {
user.setGender("male");
this.insertUser(user);
}
当外部方法直接调用 insertMale(user)
的时候,事务并不会生效。
6.2 原因分析
AOP使用的是动态代理的机制,它会给类生成一个代理类,事务的相关操作都在代理类上完成。内部调用使用的是实例调用,并没有通过代理类调用方法,所以会导致事务失效。
6.2.1 伪代码
- 代理类
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理之前做增强
System.out.println("代理之前...");
//根据需要添加事务处理逻辑
...
//调用原有方法 insertMale
Object obj = method.invoke(object, args);
//做增强
System.out.println("代理之后...");
//根据需要添加事务处理逻辑
...
return obj;
}
当执行 insertMale()
方法时,因为没有事务注解,所以没有添加事务处理逻辑,所以直接调用了目标类的 insertMale()
方法。
- 目标类执行情况
public void insertMale(User user) {
user.setGender("male");
//这里的 this 指向了目标类而不是代理类
//所以及时下面的方法添加了事务注解,但是并没有除法增强实现,事务也还是不生效的
this.insertUser(user);
}
6.3 解决方案
6.3.1 注入自身
利用Spring可以循环依赖来解决问题
@Service
public class TestService {
@Autowired
private TestService testService;
@Transactional(rollbackFor = RuntimeException.class)
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException("");
}
/**
* 内部调用新增方法
*/
public void insertMale(User user) {
user.setGender("male");
//这里使用 字段 testService 调用事务方法
testService.insertUser(user);
}
}
6.3.2 使用 ApplicationContext 获取目标类
注入 Spring 上下文 ApplicationContex
, 然后获取到 目标 bean, 再调用事务方法
@Service
public class TestService {
@Autowired
private ApplicationContext applicationContext;
@Transactional(rollbackFor = RuntimeException.class)
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException("");
}
/**
* 内部调用新增方法
*/
public void insertMale(User user) {
user.setGender("male");
//这里使用上下文获取目标类实例
TestService testService = applicationContext.getBean(TestService.class);
testService.insertUser(user);
}
}
6.3.3 使用 AopContext
Aop 上下文采用 ThreadLocal
保存了代理对象,可以使用 Aop 上下文来进行目标方法的调用。
使用时候要在启动类上添加 exposeProxy = true
配置
- 配置
@SpringBootApplication
//配置:导出代理对象到AOP上下文
@EnableAspectJAutoProxy(exposeProxy = true)
public class DemoApplication {
}
- 使用
public class TestService {
@Transactional(rollbackFor = RuntimeException.class)
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException("");
}
/**
* 内部调用新增方法
*/
public void insertMale(User user) {
user.setGender("male");
//使用AOP上下文获取目标代理类
TestService testService = (TestService) AopContext.currentProxy();
testService.insertUser(user);
}
}