Spring事物基础知识及AOP相关陷阱分析

目录
  • 一、事务的定义
  • 二、事务的属性
  • 三、Spring 事务的隔离级别
    • 3.1 隔离级别引出的问题
      • 3.1.1 脏读
      • 3.1.2 不可重复读
      • 3.1.3 幻读
    • 3.2 隔离级别
  • 四、Spring 事务的传播机制
  • 五、Spring 事务的应用(声明式)
    • 5.1 事务只读
      • 5.1.1 应用场景
      • 5.1.2 使用方式
    • 5.2 事务回滚
      • 5.2.1 使用方式
    • 5.3 事务超时
      • 5.3.1 使用方式
    • 5.4 事务传播机制的使用方式
    • 5.5 事务隔离机制的使用方式
  • 六、Spring 声明式事务的 AOP 陷阱
    • 6.1 AOP 代理陷阱复现
    • 6.2 原因分析
      • 6.2.1 伪代码
    • 6.3 解决方案
      • 6.3.1 注入自身
      • 6.3.2 使用 ApplicationContext 获取目标类
      • 6.3.3 使用 AopContext

一、事务的定义

事务(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事物基础 AOP陷阱内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解Spring AOP自定义可重复注解没有生效问题

    目录 1. 问题背景 2. 不啰嗦,上代码 3. 问题排查 3.1 是不是切点写得有问题,于是换成如下形式: 3.2 是不是使用的地方不是代理对象 4. 问题原因 1. 问题背景 工作中遇到这样的场景:某个方法需要在不同的业务场景下执行特定的逻辑,该方法已经上生产,不想改变原来的代码,因此决定用AOP做个切面执行逻辑. 2. 不啰嗦,上代码 以下为核心代码: 定义注解: @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(Rete

  • 详解Spring AOP

    目录 什么是AOP? AOP术语 通知(Advice) 连接点(Join point) 切点(Pointcut) 连接点和切点的区别 切面(Aspect) 引入(Introduction) 织入(Weaving) SpringAOP SpringAOP的特点 SpringBoot集成SpringAOP - 依赖引入 - 创建注解 - 定义切面 - 设置切点 - 业务接口编写 - 测试 通知时机 - 正常情况 - 异常情况 总结 什么是AOP? ​AOP,即我们平时经常提到的面向切面编程.首先我们

  • 聊聊Spring——AOP详解(AOP概览)

    目录 一.对AOP的初印象 首先先给出一段比较专业的术语: 然后我们举一个比较容易理解的例子: 二.AOP中的相关概念 这里还是先给出一个比较专业的概念定义: 然后举一个容易理解的例子: 三.其他的一些内容 Advice 的类型 一.对AOP的初印象 首先先给出一段比较专业的术语: 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术. AOP是OOP的延续,是软件开发中的一个热点,也是

  • Spring系列之事物管理

    目录 前言 Spring事务抽象 Spring之编程式事物 声明式事物 事物失效的8种情况及解决办法 前言 我们都知道Spring给我们提供了很多抽象,比如我们在操作数据库的过程中,它为我们提供了事物方面的抽象,让我们可以非常方便的以事物方式操作数据库.不管你用JDBC.Mybatis.Hibernate等任何一种方式操作数据库,也不管你使用DataSource还是JTA的事物,Spring事物抽象管理都能很好的把他统一在一起.接下来看一下事物的抽象核心接口 Spring事务抽象 Platfor

  • Spring AOP使用接口方式实现

    目录 一. 环境准备 二.Spring接口方式实现AOP步骤 1. 业务接口实现 2. 业务类 3. 通知类 4. 自定义切## 点 5.配置xml文件 6. 方法入口 三. 分析 Spring 提供了很多的实现AOP的方式:Spring 接口方式,schema配置方式和注解. 本文重点介绍Spring使用接口方式实现AOP. 研究使用接口方式实现AOP, 以了解为目的. 更好地理解spring使用动态代理实现AOP. 通常我们使用的更多的是使用注解的方式实现AOP 下面来看看如何实现接口方式的

  • Spring事物基础知识及AOP相关陷阱分析

    目录 一.事务的定义 二.事务的属性 三.Spring 事务的隔离级别 3.1 隔离级别引出的问题 3.1.1 脏读 3.1.2 不可重复读 3.1.3 幻读 3.2 隔离级别 四.Spring 事务的传播机制 五.Spring 事务的应用(声明式) 5.1 事务只读 5.1.1 应用场景 5.1.2 使用方式 5.2 事务回滚 5.2.1 使用方式 5.3 事务超时 5.3.1 使用方式 5.4 事务传播机制的使用方式 5.5 事务隔离机制的使用方式 六.Spring 声明式事务的 AOP 陷

  • 交换机基础知识与常见相关术语

    交换机英文名称为Switch,也称为交换式集线器,它是一种基于MAC地址(网卡的硬件标志)识别,能够在通信系统中完成信息交换功能的设备.其工作原理可以简单地描述为"存储转发"四个字,比如有两台计算机(A和B)通过交换机来连接,如果A要向B传输数据,交换机首先可以将连接到A端口发送的信息先储存下来,然后查找交换机内的MAC地址列表,每一个MAC地址对应一台计算机,找到后会与B之间架起一条临时的专用数据通道,并将数据发送到B中(见图).因为交换机支持"全双工"模式,所以

  • Java基础知识精通数组的内存分析

    目录 1.数组内存图 2.两个数组的内存图 3.一个变量两个数组容器的内存图 4.两个变量指向一个数组容器的内存图 前言:本文章主要讲解数组的内存图,更好地掌握数组以及数组调用流程,话不多说开讲. 1.数组内存图 1.一个数组的内存图 int[] arr = new int[3]; //刚开始定义数组arr为int型包含三位数字,初始化为0,0,0. arr[0] = 12; //数组arr第一位被赋值12 arr[2] = 14; //数组arr第三位被赋值14 System.out.prin

  • Spring+SpringMVC+MyBatis深入学习及搭建(一)之MyBatis的基础知识

    1.对原生态jdbc程序中问题总结 1.1 jdbc程序 需求:使用jdbc查询mysql数据库中用户表的记录 statement:向数据库中发送一个sql语句 预编译statement:好处:提高数据库性能. 预编译statement向数据库中发送一个sql语句,数据库编译sql语句,并把编译的结果保存在数据库砖的缓存中.下次再发sql时,如果sql相同,则不会再编译,直接使用缓存中的. jdbc编程步骤: 1. 加载数据库驱动 2. 创建并获取数据库链接 3. 创建jdbc statemen

  • vue-router相关基础知识及工作原理

     前言 今天面试被问到 vue的动态路由,我竟然没有回答上来,感觉不是什么难得问题.好久没有看vue-router的文档,很多用的东西和概念没有对上.回来一看什么是动态路由就傻眼了.看来有必要把vue -router相关知识总结一下,好丢人的感觉. 单页面应用的工作原理 我理解的单页面工作原理是通过浏览器URL的#后面的hash变化就会引起页面变化的特性来把页面分成不同的小模块,然后通过修改hash来让页面展示我们想让看到的内容. 那么为什么hash的不同,为什么会影响页面的展示呢?浏览器在这里

  • JavaSwing基础之Layout布局相关知识详解

    一.View layout方法 首先,还是从ViewRootImpl说起,界面的绘制会触发performMeasure.performLayout方法,而在performLayout方法中就会调用mView的layout方法开始一层层View的布局工作. private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { final View ho

  • Java基础之反射技术相关知识总结

    一.反射概念 Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法.这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制.反射被视为动态语言的关键. 二.反射应用场景 1.几乎所有的框架都会用到反射 2.程序解耦合使用 3.代码更加的优雅 三.反射更多细节 1.Jdk中的位置: java.lang.reflect包下 2.获取字节码方式 //

  • Java基础学习之运算符相关知识总结

    1.算术运算符 +表示加法运算符 -表示减法运算符 *表示乘法运算符 /表示除法运算符 %表示取模/取余运算符 package com.lagou.Day03; /** * 算术运算符 */ public class Demo01 { public static void main(String[] args) { //1.声明两个int类型的变量并初始化 //int ia = 6;ib = 2;//表示声明两个int类型的变量ia和ib,不推荐使用 int ia = 6; int ib = 2

  • Python基础之变量的相关知识总结

    变量全都是引用 跟其他编程语言不同,Python的变量不是盒子,不会存储数据,它们只是引用,就像标签一样,贴在对象上面. 比如: >>> a = [1, 2, 3] >>> b = a >>> a.append(4) >>> b [1, 2, 3, 4] >>> b is a True a变量和b变量引用的是同一个列表[1, 2, 3].b可以叫做a的别名. 比较来看: >>> a = [1, 2,

  • Python基础之循环语句相关知识总结

    目录 一.循环语句介绍 二.循环语句的分类 三.循环控制语句 四.while循环 五.break和continue 六.for循环 七.pass语句的使用 一.循环语句介绍  1.循环语句理解 循环语句允许我们执行一个语句或语句组多次,可以让我们的代码重复的去执行. 2.循环语句示意图 二.循环语句的分类 三.循环控制语句 四.while循环 while循环解释:判断语句的条件是否为真,如果为真,则执行代码,然后再次判断条件,直到条件为假为止,循环结束. 1.while死循环 # 死循环示例 w

随机推荐