Spring事务基础入门及AOP陷阱分析

更新说明:

  • 2021-12-28

6.2.1 章节更新了代理说明图,调整了说明顺序,修复了错误说明

转载请注明出处: https://www.cnblogs.com/qnlcy/p/15237377.html

一、事务的定义

事务(Transaction),是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit),是恢复和并发控制的基本单位。

事务的产生,其实是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.

二、事务的属性

事务具有4个属性,简称 ACID

属性 说明 Atomicity 原子性 一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。 Consistency 一致性 事务执行的结果必须是使数据库从一个一致性状态c0变到另一个一致性状态c1 Isolation 隔离性 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 Durability 持久性 指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

三、Spring 事务的隔离级别

当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。

在介绍数据库提供的各种隔离级别之前,我们先看看如果不考虑事务的隔离性,会发生的几种问题

3.1 隔离级别引出的问题

3.1.1 脏读

是指在没有隔离的情况下,一个事务读取了另外一个事务已修改但未提交(有可能回滚也有可能继续修改)的缓冲区数据。

Spring事务基础入门及AOP陷阱分析

3.1.2 不可重复读

数据库中的某项数据在一个事务多次读取,但是在多次读取期间,其他事务对其有修改并提交,导致返回值不同,这就发生了不可重复读。

不可重复读侧重修改。

Spring事务基础入门及AOP陷阱分析

3.1.3 幻读

幻读和不可重复读相似。当一个事务(T1)读取几行记录后(事务并没有结束),另一个并发事务(T2)插入了一些记录时,幻读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。

幻读侧重新增或者删除。

Spring事务基础入门及AOP陷阱分析

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 声明式事务是指依托注解 @TransactionalAOP 功能,在其方法两端添加事务的操作,实现对被注解修饰方法的 增强

5.1 事务只读

从事务开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)。

事务只读只适用于 当传播机制为 PROPAGATION_REQUIRED, PROPAGATION_REQUES_NEW 的情况

5.1.1 应用场景

在诸如统计查询、报表查询的过程当中,需要多次查询,为了避免在查询过程当中对剩余查询数据的修改,保证数据整体在某一时刻的一致性,需要使用只读事务。

5.1.2 使用方式

@Transactional(propagation = Propagation.REQUIRES, readOnly = true)
public List 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使用的是动态代理的机制,它会给类生成一个代理类,事务的相关操作都在代理类上完成。内部调用使用的是实例调用,并没有通过代理类调用方法,所以会导致事务失效。

Spring事务基础入门及AOP陷阱分析

6.2.1 伪代码

根据上图提示,我们来分析 AOP 陷阱的执行流程

  • 1.目标类执行情况
    public void insertMale(User user) {
        user.setGender("male");
        //这里的 this 指向了目标类而不是代理类
        //所以即使下面的方法添加了事务注解,但是并没有除法增强实现,事务也还是不生效的
        this.insertUser(user);
    }

重点在于,最后一行代码当中的 this,根据上图得知, insertMale 方法并没有增强,所以这里的 this 指向的是目标类本身

  • 2.代理类
    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() 没有添加事务处理逻辑,所以 this.insertUser()直接调用了目标类本身没有被增强处理的 insertUser() 方法

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);
    }
}

Original: https://www.cnblogs.com/qnlcy/p/15237377.html
Author: 去哪里吃鱼
Title: Spring事务基础入门及AOP陷阱分析

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/577908/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • .NET Core 3.0, 发布将于今晚开始!

    期待已久的.NET Core 3.0即将发布! .NET Core 3.0在.NET Conf上发布。大约还有9个多小时后,.NET Conf开始启动。 第1天-9月23日 9:0…

    Linux 2023年6月7日
    082
  • shell 获取进程号

    Shell最后运行的后台PID(后台运行的最后一个进程的进程ID号) $! Shell本身的PID(即脚本运行的当前进程ID号 $$ Original: https://www.c…

    Linux 2023年5月28日
    088
  • 企业项目开发流程

    企业项目开发流程 商城 1.1 B2C 直销商城 商家与会员直接交易 ( Business To Customer ) 1.2 B2B 批发商城 商家与商家直接交易 1.3 B2B…

    Linux 2023年6月14日
    092
  • 一文搞懂 Redis 架构演化之路

    作者:ryetan,腾讯 CSIG 后台开发工程师 现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是…

    Linux 2023年5月28日
    0101
  • JavaWeb创建一个公共的servlet

    对于初学者来说,每次前端传数据过来就要新建一个类创建一个doget、dopost方法,其实铁柱兄在大学的时候也是这么玩的。后面铁柱兄开始认真了,就想着学习点容易的编程方式,其实说白…

    Linux 2023年6月13日
    094
  • 微信小程序开发(1)

    微信小程序开发(1) 微信小程序开发(1) 代码结构与基本配置 代码结构与基本配置 基本HelloWorld创建 开发框架——基本构成 微信开发者工具 版本控制 WXML wxml…

    Linux 2023年6月8日
    096
  • [转帖]shell中if语句的使用

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Linux 2023年5月28日
    0104
  • wordpress固定链接+宝塔nginx配置伪静态访问URL

    一、站点设置 打开站点设置,选择伪静态,选择wordpress 二、wordpress设置 打开wordpress后台,选择 设置 —》固定链接 选择一个你喜欢的格式点…

    Linux 2023年6月14日
    099
  • ubuntu 20.04.1 安装 PHP+Nginx

    ubuntu 20.04.1 安装 PHP+Nginx 全流程 ubuntu 20.04.1 安装 PHP+Nginx 更新源 sudo apt-get update 安装环境包 …

    Linux 2023年6月7日
    0131
  • k8s 常用命令

    查看所有 pod 列表, -n 后跟namespace,查看指定的命名空间 查看 RC 和service 列表,-o wide 查看详细信息 显示 Node 的详细信息 显示 Po…

    Linux 2023年5月27日
    0115
  • RestFul风格

    概念:Restful就是一个资源定位及资源操作的风格。不是标准也不是协议,只是一种风格。基于这个风格 设计的软件可以更简洁,更有层次,更易于实现缓存等机制。 功能资源:互联网所有的…

    Linux 2023年6月14日
    089
  • zabbix用户,角色,权限,模板管理

    zabbix用户,角色,权限,模板管理 用户组 用户角色 用户 使用刚才创建的用户登录 模板组 模板 模板的监控项可以自己创建也可以从其他模板复制 posted @2022-09-…

    Linux 2023年6月13日
    0114
  • 3.20 什么是环境变量,Linux环境变量有哪些?

    变量是计算机系统用于保存可变值的数据类型,我们可以直接通过变量名称来提取到对应的变量值。在 Linux 系统中,环境变量是用来定义系统运行环境的一些参数,比如每个用户不同的家目录(…

    Linux 2023年6月7日
    0100
  • powershell版,Fail2Ban脚本,阻止黑客攻击sshd

    关键字 powershell Deny Hosts Fail2Ban ssh linux 近期惊闻 黑客团伙利用SSH暴力破a解,入侵远程设备 用于挖矿和DDoS攻击 疑似来自罗马…

    Linux 2023年6月14日
    078
  • Docker容器网络配置

    Docker容器网络配置 1、Linux内核实现名称空间的创建 1.1 ip netns命令 可以借助 ip netns命令来完成对 Network Namespace 的各种操作…

    Linux 2023年6月7日
    095
  • git 那些事儿 —— 基于 Learn Git Branching

    推荐一个 git 图形化教学网站:Learn Git Branching,这个网站有一个沙盒可以直接在上面模拟 git 的各种操作,操作效果使用图形的方式展示,非常直观。本文可以看…

    Linux 2023年5月27日
    0127
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球