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);
}
}
Spring事物入门简介及AOP陷阱分析的更多相关文章
- Spring事物管理简介 (转)
一.事物1.什么是事物 事物指的是逻辑上的一组操作,这组操作要么全部成功,要么全部失败 2.事物的特性 原子性:事物是一个不可分割的工作单位,事物中的操作要么都发生,要么都不发生 一致性:事物前后数据 ...
- Spring的入门学习笔记 (AOP概念及操作+AspectJ)
AOP概念 1.aop:面向切面(方面)编程,扩展功能不通过源代码实现 2.采用横向抽取机制,取代了传统的纵向继承重复代码 AOP原理 假设现有 public class User{ //添加用户方法 ...
- spring boot (入门简介 demo)
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置.通过 ...
- Spring Boot入门简介-Maven配置
一.简介 -- 简化Spring应用开发的一个框架: -- 整个Spring技术栈的一个大整合: -- J2EE开发的一站式解决方案. 二.背景: ① J2EE笨重的开发.繁多的配置.低下的开发效率. ...
- Spring(1) --入门(IoC,AOP)
说说你对spring的理解? Spring框架是一个轻量级的企业级开发的一站式解决方案,所谓一站式解决方案就是可以基于Spring解决Java EE开发的所有问题.Spring框架主要提供了IoC容器 ...
- Spring框架入门之AOP
Spring框架入门之AOP 一.Spring AOP简单介绍 AOP AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented ...
- JAVA基础加强(张孝祥)_类加载器、分析代理类的作用与原理及AOP概念、分析JVM动态生成的类、实现类似Spring的可配置的AOP框架
1.类加载器 ·简要介绍什么是类加载器,和类加载器的作用 ·Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader ...
- Spring Boot 入门(五):集成 AOP 进行日志管理
本篇文章是接着 Spring boot 入门(四):集成 Shiro 实现登陆认证和权限管理写的,按照前面几篇博客的教程,可以搭建一个简单的项目,主要包含了 Pagehelper+MyBatis 分页 ...
- Spring Boot 入门详细分析
推荐阅读: 我们为什么要学习 Spring Boot 我们搭建 Spring Boot 项目,可以使用 Spring 为我们提供的初始化网站,那个可能不太方便,今天呢,我们就来说说如何使用 IDEA ...
随机推荐
- 🔥 LeetCode 热题 HOT 100(1-10)
1. 两数之和 思路一:暴力遍历所有组合 class Solution { public int[] twoSum(int[] nums, int target) { for (int i = 0; ...
- iTop安装 vm虚拟机、Linux、centos7安装itop 2.6.1
itop安装流程,是我基于下面两位博主发布的文章整理出来的,欢迎大家学习,如有错误之处请大家留言指出我看到之后及时更新.谢谢 https://blog.csdn.net/qq_23565543/art ...
- 3.python编程与计算机的关系,如何执行python文件
上一节预告了这一章想讲如何不停地和世界打招呼,这涉及到编程中一个重要的概念:循环. 但经过了两周断更后细想了一下,不行,我们得对上一章进行补充,而且这个补充非常关键!也印证了上一章所说的: 上一节章很 ...
- 洛谷P1879题解
题面 显然是个状压DP. 看数据范围,不难发现算法复杂度应该是 \(O(n\times 2^n \times 2^n)\) . 显然第一个 \(n\) 是遍历每一行的土地. 后面两个 \(2^n\) ...
- OpenFaaS实战之八:自制模板(maven+jdk8)
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- fiddler 之 返回数据乱码解决方法
1.有时用fiddler抓包, 发现抓到的包, 发送数据和返回数据都是乱码, 怎么办? 直接上图 (这办法不是100%成功的) 方法一: 方法二:
- Java容器学习——List
Java容器学习--List 基础知识 数组: 优点:随机存取,可以快速访问元素 缺点:静态分配内存,存在空间闲置或者溢出现象:不适合进行插入和删除操作,需要移动大量元素. 链表: 优点: ...
- Python3中的字符串相关操作
Python3的字符串操作相关函数详解 字符串内建函数 1. capitalize() 将字符串中的第一个字符转换成大写,其他字母变成小写.例: >>> "hello Wo ...
- 从MySQL大量数据清洗到TiBD说起
从MySQL大量数据清洗到TiBD说起 一.业务场景: 公司主要做的业务是类似贝壳的二手房租售,公司数据库存了上亿级别的房源数据,之前的数据库使用的是 mysql,后面需要将mysql数据库切换成了 ...
- 解决uni-file-picker组件在ios点击失效的问题
从组件源码里找到choose-and-upload-file.js文件,查找chooseAndUploadFile方法,在方法里添加下面代码,保存即可解决. opts.sourceType = ['a ...