百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Spring 中的事务 和 事务的传播机制

bigegpt 2024-08-01 12:02 13 浏览

1. 事务是什么?为什么需要事务?

??事务是一种用于管理数据库操作的机制。它将一组操作封装成一个单元,确保数据库操作要么全部成功提交,要么全部回滚,以保持数据的一致性和完整性。

??为什么要使用事务呢?请看下面的案例。

??假设有两个用户的银行账户,账户A和账户B,它们分别存储着一定的金额。现在,用户A想要向用户B转账100元。这个转账操作需要以下两个步骤:

  1. 从用户A的账户中扣除100元。
  2. 将扣除的100元添加到用户B的账户中。

??在这个过程中,我们需要确保两个步骤要么同时成功提交,要么同时回滚。如果第一步执行成功了,第二步却执行失败了,那么B没有收到这100块钱,A的钱就不翼而飞了。所以如果其中一个步骤出现问题,我们必须回滚整个事务,以保持数据的一致性。

2. Spring 中事务的实现

2.1 编程式事务

??Spring Boot中内置了两个对象,即:DataSourceTransactionManager与 TransactionDefinition,用这两个对象就可以来操作事务了。

??这里已经配置了相应的数据库环境

java复制代码@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    //DataSourceTransactionManager: 数据源事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

	//TransactionDefinition:事务定义    
    @Autowired
    private TransactionDefinition transactionDefinition;

    //根据 id 删除数据
    @RequestMapping("/delete")
    public Integer delete(Integer id){
        //开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

        //对数据库操作:删除
        Integer result = userService.delete(1);

        //提交事务 or 回滚事务
        //回滚事务
        dataSourceTransactionManager.rollback(transactionStatus);
        //提交事务
        //dataSourceTransactionManager.commit(transactionStatus);
        return result;
    }
  • 使用DataSourceTransactionManager的getTransaction方法开始一个事务。
  • 在getTransaction方法中传递一个TransactionDefinition对象来定义事务的属性。
  • getTransaction方法返回一个TransactionStatus对象,表示当前事务的状态。
  • 在事务执行过程中,可以通过TransactionStatus对象来检查事务的状态。
  • 最终,通过调用dataSourceTransactionManager的commit或rollback方法提交或回滚事务。

??下面是更为完整、规范的代码:

java复制代码@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private DataSourceTransactionManager transactionManager;

    @Autowired
    private TransactionDefinition transactionDefinition;

    @RequestMapping("/delete")
    public Integer delete(Integer id){
        if(id == null || id <= 0){
            return 0;
        }

        TransactionStatus transactionStatus = null;
        int result = 0;
        try{
            //开启事务
            transactionStatus = transactionManager.getTransaction(transactionDefinition);
            //业务操作,删除事务
            result = userService.delete(id);
            System.out.println("删除:" + result);
            //提交事务
            transactionManager.commit(transactionStatus);
        }catch (Exception e){
            //回滚事务
            if(transactionStatus != null){
                transactionManager.rollback(transactionStatus);
            }
        }
        return result;
    }
}

??但是这种方式太繁琐了,还有更为简单的方法。

2.2 声明式事务(注解)

??使用 @Transactional 注解:

java复制代码@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/delete2")
    @Transactional
    public Integer delete2(Integer id){
        if(id == null || id <= 0){
            return 0;
        }
        int result = userService.delete(id);
        System.out.println("删除:" + result);
        return result;
    }
}

??无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务

??待删除数据的表,这里删除“张三”:

??进行访问后:

说明事务提交成功。

2.2.1 发生异常的时候

(1)没有处理的异常会自动回滚事务

??下面的代码会抛出异常,这时候再看看事务是否会回滚。

java复制代码@RequestMapping("/delete2")
@Transactional
public Integer delete2(Integer id){
    if(id == null || id <= 0){
        return 0;
    }
    int result = userService.delete(id);
    int x = 8 / 0; //会抛出 ArithmeticException 异常
    System.out.println("删除:" + result);
    return result;
}

中途发生了没有处理的异常会自动回滚事务

(2)处理后的异常不会自动回滚事务

java复制代码@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/delete2")
    @Transactional
    public Integer delete2(Integer id){
        if(id == null || id <= 0){
            return 0;
        }
        int result = 0;
        try {
            //删除数据
            result = userService.delete(id);
            System.out.println("删除:" + result);
            int x = 8 / 0;
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        return result;
    }
}

??访问前的表:

??删除 id=4:

??访问后的表:

??可以看到处理了异常后,事务没有回滚,这样的操作非常的危险,但是也有解决的方法,那就是手动回滚事务:

java复制代码@RequestMapping("/delete2")
@Transactional
public Integer delete2(Integer id){
    if(id == null || id <= 0){
        return 0;
    }
    int result = 0;
    try {
        result = userService.delete(id);
        System.out.println("删除:" + result);
        int x = 8 / 0;
    }catch (Exception e){
        System.out.println(e.getMessage());
        //手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return result;
}

??这样它就会回滚事务了。

2.2.2 @Transactional 作用范围

??@Transactional 可以加在方法上以及类上,但是:

  • 当使用 @Transactional 注解修饰方法时,它只对public的方法生效。
  • 当使用 @Transactional 注解修饰类时,表示对该类中所有的public方法生效。

??这是因为基于代理的事务管理机制在运行时创建代理对象,并且代理对象只能访问public方法。当@Transactional注解应用于非public方法(如protected、private或默认包可见性的方法)时,代理对象无法访问这些方法,导致事务管理无法生效。

2.2.3 @Transactional 参数说明

下面是补充的 @Transactional 注解的参数及其作用的汇总表格:

参数

描述

value

指定事务管理器的名称。

propagation

指定事务的传播行为。

isolation

指定事务的隔离级别。

readOnly

指定事务是否为只读。

timeout

指定事务的超时时间(以秒为单位)。

rollbackFor

指定哪些异常触发事务回滚。

rollbackForClassName

指定哪些异常类名触发事务回滚。

noRollbackFor

指定哪些异常不触发事务回滚。

noRollbackForClassName

指定哪些异常类名不触发事务回滚。

  1. propagation:指定事务的传播行为(后文详细介绍)。
  2. isolation:指定事务的隔离级别,定义了事务之间的可见性和并发控制(后文详细介绍)。
  3. readOnly:指定事务是否为只读,如果设置为 true,则表示该事务只读取数据,不修改数据。
  4. timeout:指定事务的超时时间,单位为秒。如果事务执行时间超过指定的超时时间,则事务会被强制回滚。
  5. rollbackFor:指定哪些异常触发事务回滚。可以指定一个或多个异常类型的数组。
  6. rollbackForClassName:指定哪些异常类名触发事务回滚。可以指定一个或多个异常类名的字符串数组。
  7. noRollbackFor:指定哪些异常不触发事务回滚。可以指定一个或多个异常类型的数组。
  8. noRollbackForClassName:指定哪些异常类名不触发事务回滚。可以指定一个或多个异常类名的字符串数组。

2.2.3 @Transactional 工作原理

??Spring 通过代理模式实现 @Transactional 的工作原理。当一个带有 @Transactional 注解的方法被调用时,Spring 将创建一个代理对象来管理事务。Spring 使用 AOP(面向切面编程)将事务管理逻辑织入到带有 @Transactional 注解的方法周围。这样,在方法执行前后,会插入事务管理相关的代码。

下面是方法调用的详细过程:

  1. 调用者通过代理对象调用被代理的方法。
  2. 代理对象接收到方法调用请求。
  3. 代理对象在方法调用前执行预定义的逻辑,例如事务管理的开始。
  4. 代理对象将实际的方法调用委托给原对象。这意味着代理对象将真正的方法调用传递给原对象,使原对象执行实际的业务逻辑。
  5. 原对象执行方法的实际逻辑。
  6. 原对象返回方法的结果给代理对象。
  7. 代理对象在方法调用后执行额外的逻辑,例如事务管理的提交或回滚。
  8. 代理对象将方法的结果返回给调用者。

3. 事务的隔离级别

3.1 事务特性

事务具有以下四个重要的特性,通常被称为 ACID 特性:

  1. 原子性(Atomicity):原子性要求事务被视为不可分割的最小工作单元,要么全部执行成功,要么全部失败回滚。事务在执行过程中发生错误或中断,系统必须能够将其恢复到事务开始前的状态,保证数据的一致性。
  2. 一致性(Consistency):一致性确保事务在执行前后数据库的状态是一致的。事务在执行过程中对数据库进行的修改必须满足预定义的规则和约束,以保证数据的完整性。
  3. 隔离性(Isolation):隔离性指多个事务并发执行时,每个事务的操作都应当与其他事务相互隔离,使它们感觉不到其他事务的存在。隔离性可以防止并发执行的事务之间发生干扰和数据冲突,确保数据的正确性。
  4. 持久性(Durability):持久性要求事务一旦提交,其对数据库的修改就是永久性的,即使在系统发生故障或重启的情况下,修改的数据也能够被恢复。持久性通过将事务的结果写入非易失性存储介质(如磁盘)来实现。

3.2 事务的隔离级别

对于隔离性,通常有以下四个标准的隔离级别:

  1. Read Uncommitted(读取未提交数据):最低的隔离级别。在该级别下,一个事务可以读取到另一个事务未提交的数据,可能导致脏读,即读取到了未经验证的数据。这个级别会导致数据的不一致性,并且不提供任何并发控制。
  2. Read Committed(读取已提交数据):在该级别下,一个事务只能读取到已经提交的数据。它避免了脏读,但可能出现不可重复读(Non-repeatable Read)的问题。不可重复读是指同一个事务中多次读取同一数据,在事务执行过程中,该数据被其他事务修改,导致每次读取到的值不一致。
  3. Repeatable Read(可重复读):在该级别下,一个事务在执行期间多次读取同一数据时,保证能够读取到一致的结果。即使其他事务对该数据进行修改,也不会影响当前事务的读取操作。这个级别通过锁定读取的数据,避免了不可重复读,但可能出现幻读(Phantom Read)的问题。幻读是指同一个事务中多次查询同一个范围的数据时,由于其他事务插入了新的数据,导致每次查询结果集不一致。
  4. Serializable(可串行化):最高的隔离级别,它要求事务串行执行,完全避免了并发问题。在该级别下,事务之间互相看不到对方的操作,可以避免脏读、不可重复读和幻读等问题。然而,由于串行化执行,会牺牲一定的并发性能。

3.3 Spring 中设置隔离级别

??在Spring中,可以使用@Transactional注解设置事务的隔离级别。Spring提供了与数据库事务隔离级别对应的五个常量:

  1. DEFAULT:使用数据库的默认隔离级别。
  2. READ_UNCOMMITTED:对应数据库的读取未提交数据(Read Uncommitted)隔离级别。
  3. READ_COMMITTED:对应数据库的读取已提交数据(Read Committed)隔离级别。
  4. REPEATABLE_READ:对应数据库的可重复读(Repeatable Read)隔离级别。
  5. SERIALIZABLE:对应数据库的可串行化(Serializable)隔离级别。

??使用@Transactional注解时,可以通过isolation属性指定事务的隔离级别。例如:

java复制代码@Transactional(isolation = Isolation.READ_COMMITTED)
public void myMethod() {
    // 事务处理逻辑
}

4. Spring 事务传播机制

4.1 什么是事务的传播机制?

??事务传播机制是指定事务在方法调用之间如何传播和影响的机制,通过定义事务的传播行为,控制事务在不同方法之间的创建、挂起、恢复和回滚操作。

??下面是常见的事务传播行为:

  1. REQUIRED(默认):如果当前存在事务,则加入到当前事务中,如果没有事务,则创建一个新的事务。
  2. SUPPORTS:如果当前存在事务,则加入到当前事务中,如果没有事务,则以非事务的方式执行。
  3. MANDATORY:必须在一个已存在的事务中执行,否则抛出异常。
  4. REQUIRES_NEW:每次都会创建一个新的事务,如果当前存在事务,则将当前事务挂起。
  5. NOT_SUPPORTED:以非事务的方式执行操作,如果当前存在事务,则将当前事务挂起。
  6. NEVER:必须以非事务方式执行,如果当前存在事务,则抛出异常。
  7. NESTED:如果当前存在事务,则在嵌套事务内执行,如果没有事务,则创建一个新的事务。

??"当前存在事务"指的是在方法调用期间已经开启的事务。在Spring中,事务是基于线程的,每个线程都有一个事务上下文。如果在方法调用期间已经存在一个事务上下文(即已经开启了一个事务),则可以说"当前存在事务"。

??当一个方法被调用时,Spring会检查当前线程是否已经有一个事务上下文存在。如果有,那么这个方法就可以在这个已存在的事务上下文中执行,即在当前事务中执行。方法可以访问和操作当前事务中的数据,并共享该事务的一致性和隔离级别(取决于方法的事务传播行为设置)。

??如果当前线程没有事务上下文存在,那么方法可以选择创建一个新的事务,或者以非事务方式执行。这取决于方法的事务传播行为设置。新的事务上下文会在方法开始时创建,并在方法执行完毕后进行提交或回滚。

??例如,一个方法A内部调用了另一个方法B,如果方法B具有REQUIRED(默认)的事务传播行为,而方法A已经在一个事务中执行,那么方法B将加入到方法A的事务中,共同参与事务的操作。

4.2 事务传播机制的演示

??本篇只演示一部分。

4.2.1 准备工作

??在演示之前,这里先创建两张表,以方便我们看出它们的作用。

??插入的数据:

??log 表为空:

??定义3个类:

java复制代码@RestController
@RequestMapping("/user3")
public class UserController3 {

    @Autowired
    private UserService userService;

    // REQUIRED 类型
    @RequestMapping("/add")
    @Transactional(propagation = Propagation.REQUIRED)
    public int add(String username,String password){
        if(username == null || password == null || username.equals("") || password.equals("")){
            return 0;
        }
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername(username);
        userInfo.setPassword(password);
        int result = userService.add(userInfo);
        return result;
    }
}
java复制代码@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private LogService logService;

    public Integer delete(int id){
        return userMapper.delete(id);
    }

    // REQUIRED 类型
    //添加用户
    @Transactional(propagation = Propagation.REQUIRED)
    public Integer add(UserInfo userInfo){
        //给用户表添加用户信息
        int addUserResult = userMapper.add(userInfo);
        System.out.println("添加用户结果:" + addUserResult);
        Log log = new Log();
        log.setMessage("添加日志信息");
        logService.add(log);
        return 0;
    }
}
java复制代码@Service
public class LogService {

    @Autowired
    private LogMapper logMapper;
    
    //添加日志信息
	// REQUIRED 类型
    @Transactional(propagation = Propagation.REQUIRED)
    public Integer add(Log log){
        int result = logMapper.add(log);
        System.out.println("添加日志的结果:" + result);
        //回滚事务,模仿发生异常,这里为什么不写一个异常呢?因为异常会传递到外面的方法。
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return result;
    }
}

4.2.2 REQUIRED 演示

??调用关系:

??传入以下的值:

??可以看到,两个表都是添加成功的,但是LogService中的add方法回滚了,重点看其它方法回滚了没有:

??这两个表没有变化,说明所有的方法都是回滚了的。这就体现了REQUIRED这个传播行为,一个方法回滚了,其它所有方法都回滚。

??更具体的:LogService中的add方法本身有事务,UserService中的add方法也是REQUIRED。这时候,LogService中的add就加入了UserService中的事务,相当于一个整体。

4.2.3 REQUIRES_NEW 演示

??将方法都改为REQUIRES_NEW,方法调用跟上面一样。

java复制代码@RequestMapping("/add")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(String username,String password){
    if(username == null || password == null || username.equals("") || password.equals("")){
        return 0;
    }
    UserInfo userInfo = new UserInfo();
    userInfo.setUsername(username);
    userInfo.setPassword(password);
    int result = userService.add(userInfo);
    return result;
}
java复制代码//添加用户
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer add(UserInfo userInfo){
    //给用户表添加用户信息
    int addUserResult = userMapper.add(userInfo);
    System.out.println("添加用户结果:" + addUserResult);
    Log log = new Log();
    log.setMessage("添加日志信息");
    logService.add(log);
    return 0;
}
java复制代码@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer add(Log log){
    int result = logMapper.add(log);
    System.out.println("添加日志的结果:" + result);
    //回滚事务,模仿发生异常,这里为什么不写一个异常呢?因为异常会传递到外面的方法。
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    return result;
}

??发送请求:

??结果:

??UserService中add方法的事务没有回滚,LogService中的事务回滚了,回忆REQUIRES_NEW:每次都会创建一个新的事务,如果当前存在事务,则将当前事务挂起。在这里,LogService中的事务先执行,执行完后再执行UserService中的事务。

4.2.4 NESTED(嵌套事务)演示

??同样的,把@Transactional改为NESTED。

??请求:

??数据库中的表:

??LogService中的事务已经回滚,但是嵌套事务不会回滚嵌套之前的事务,也就是说嵌套事务可以实现部分事务回滚,但是这与上面的REQUIRES_NEW是一样的效果呀,它们有什么区别呢?

4.2.5 NESTED(嵌套事务)与 REQUIRES_NEW的区别

  1. NESTED(嵌套事务):
  2. ??在嵌套事务中,内部事务实际上是由外部事务开启和提交/回滚的。 当外部事务回滚时,会导致内部事务也被回滚,即使内部事务已经执行了一些提交操作。 ??这是因为嵌套事务的模拟通过保存和恢复事务状态来实现,当外部事务回滚时,它会回滚到开启内部事务的那个点,包括内部事务执行的任何修改或提交。这样可以确保事务的一致性。
  3. REQUIRES_NEW:

??REQUIRES_NEW表示创建一个独立的事务。当一个事务(外部事务)调用另一个带有REQUIRES_NEW传播行为的事务时,内部事务将在一个新的事务中执行,独立于外部事务。内部事务的提交或回滚不会影响外部事务。无论外部事务是否回滚,内部事务都可以独立提交或回滚。

4.3 嵌套事务和加入事务的区别

  1. 嵌套事务(Nested Transactions): 嵌套事务是指在一个事务内部开启了另一个独立的事务。 嵌套事务可以在父事务的范围内执行,并且具有独立的事务日志和回滚机制。 嵌套事务允许在父事务中进行更细粒度的操作和控制,例如,在一个长事务中的某个步骤中开启了一个子事务,子事务可以独立提交或回滚,而不会影响父事务的其他步骤。嵌套事务通常用于复杂的业务逻辑,可以提供更灵活的事务处理。
  2. 加入事务(Join Transactions): 加入事务是指将一个独立的事务合并到当前事务中,使它们成为一个整体。 加入事务可以将多个事务合并为一个更大的事务,确保它们作为一个原子操作进行提交或回滚。加入事务通常用于多个独立事务之间存在逻辑上的依赖关系,需要以一致的方式进行处理。通过将多个事务加入到一个事务中,可以保证它们的一致性,并且要么全部提交成功,要么全部回滚。


作者:会飞的喵喵
链接:https://juejin.cn/post/7237439385909592122

相关推荐

当Frida来“敲”门(frida是什么)

0x1渗透测试瓶颈目前,碰到越来越多的大客户都会将核心资产业务集中在统一的APP上,或者对自己比较重要的APP,如自己的主业务,办公APP进行加壳,流量加密,投入了很多精力在移动端的防护上。而现在挖...

服务端性能测试实战3-性能测试脚本开发

前言在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:阶段一:了解各个接口的入参、出参,使用Python代码模拟前端...

Springboot整合Apache Ftpserver拓展功能及业务讲解(三)

今日分享每天分享技术实战干货,技术在于积累和收藏,希望可以帮助到您,同时也希望获得您的支持和关注。架构开源地址:https://gitee.com/msxyspringboot整合Ftpserver参...

Linux和Windows下:Python Crypto模块安装方式区别

一、Linux环境下:fromCrypto.SignatureimportPKCS1_v1_5如果导包报错:ImportError:Nomodulenamed'Crypt...

Python 3 加密简介(python des加密解密)

Python3的标准库中是没多少用来解决加密的,不过却有用于处理哈希的库。在这里我们会对其进行一个简单的介绍,但重点会放在两个第三方的软件包:PyCrypto和cryptography上,我...

怎样从零开始编译一个魔兽世界开源服务端Windows

第二章:编译和安装我是艾西,上期我们讲述到编译一个魔兽世界开源服务端环境准备,那么今天跟大家聊聊怎么编译和安装我们直接进入正题(上一章没有看到的小伙伴可以点我主页查看)编译服务端:在D盘新建一个文件夹...

附1-Conda部署安装及基本使用(conda安装教程)

Windows环境安装安装介质下载下载地址:https://www.anaconda.com/products/individual安装Anaconda安装时,选择自定义安装,选择自定义安装路径:配置...

如何配置全世界最小的 MySQL 服务器

配置全世界最小的MySQL服务器——如何在一块IntelEdison为控制板上安装一个MySQL服务器。介绍在我最近的一篇博文中,物联网,消息以及MySQL,我展示了如果Partic...

如何使用Github Action来自动化编译PolarDB-PG数据库

随着PolarDB在国产数据库领域荣膺桂冠并持续获得广泛认可,越来越多的学生和技术爱好者开始关注并涉足这款由阿里巴巴集团倾力打造且性能卓越的关系型云原生数据库。有很多同学想要上手尝试,却卡在了编译数据...

面向NDK开发者的Android 7.0变更(ndk android.mk)

订阅Google官方微信公众号:谷歌开发者。与谷歌一起创造未来!受Android平台其他改进的影响,为了方便加载本机代码,AndroidM和N中的动态链接器对编写整洁且跨平台兼容的本机...

信创改造--人大金仓(Kingbase)数据库安装、备份恢复的问题纪要

问题一:在安装KingbaseES时,安装用户对于安装路径需有“读”、“写”、“执行”的权限。在Linux系统中,需要以非root用户执行安装程序,且该用户要有标准的home目录,您可...

OpenSSH 安全漏洞,修补操作一手掌握

1.漏洞概述近日,国家信息安全漏洞库(CNNVD)收到关于OpenSSH安全漏洞(CNNVD-202407-017、CVE-2024-6387)情况的报送。攻击者可以利用该漏洞在无需认证的情况下,通...

Linux:lsof命令详解(linux lsof命令详解)

介绍欢迎来到这篇博客。在这篇博客中,我们将学习Unix/Linux系统上的lsof命令行工具。命令行工具是您使用CLI(命令行界面)而不是GUI(图形用户界面)运行的程序或工具。lsoflsof代表&...

幻隐说固态第一期:固态硬盘接口类别

前排声明所有信息来源于网络收集,如有错误请评论区指出更正。废话不多说,目前固态硬盘接口按速度由慢到快分有这几类:SATA、mSATA、SATAExpress、PCI-E、m.2、u.2。下面我们来...

新品轰炸 影驰SSD多款产品登Computex

分享泡泡网SSD固态硬盘频道6月6日台北电脑展作为全球第二、亚洲最大的3C/IT产业链专业展,吸引了众多IT厂商和全球各地媒体的热烈关注,全球存储新势力—影驰,也积极参与其中,为广大玩家朋友带来了...