JavaEE16-Spring事务和事务传播机制

news2024/11/17 12:38:08

目录

1.为什么需要事务?

2.MySQL中事务的使用

3.Spring中事务的实现

3.1.编程式事务(手动写代码操作事务)

3.2.声明式事务(利用注解自动开启和提交事务)(使用为主)

3.2.1.@Transactional作用范围

3.2.2.@Transactional参数说明

3.2.3.注意事项:@Transactional + try-catch 有异常不回滚的问题

3.2.4.@Transactional工作原理

4.事务隔离级别

4.1.事务特性

①原子性(Atomicity,或称不可分割性)

②一致性(Consistency)

③隔离性(Isolation,又称独立性)

④持久性(Durability)

4.2.MySQL 事务隔离级别有4种

①READ UNCOMMITTED:读未提交(Read Uncommitted)

②READ COMMITTED:读已提交(Read Committed)

③REPEATABLE READ:可重复读(Repeatable Read)

④SERIALIZABLE:序列化(Serializable)

4.3.Spring事务隔离级别有5种

①Isolation.DEFAULT:以连接的数据库的全局事务隔离级别为主。(default)

②Isolation.READ_UNCOMMITTED:读未提交(=MySQL中的读未提交)(read_uncommitted)

③Isolation.READ_COMMITTED:读已提交(=MySQL中的读已提交)(read_committed)

④Isolation.REPEATABLE_READ:可重复读(=MySQL中的可重复读)(repeatable_read)

⑤Isolation.SERIALIZABLE:串行化(=MySQL中的串行化)(serializable)

4.4.Spring中设置事务隔离级别

5.Spring事务传播机制

5.1.事务传播机制是什么?

5.2.为什么需要事务传播机制?

5.3.事务传播机制有哪些?

①Propagation.REQUIRED:默认的事务传播级别(required)

②Propagation.SUPPORTS(supports)

③Propagation.MANDATORY(mandatory:强制性)

④Propagation.REQUIRES_NEW(requires_new)

⑤Propagation.NOT_SUPPORTED(not_supported)

⑥Propagation.NEVER(never)

⑦Propagation.NESTED(nested)

5.4.Spring事务传播机制使用和各种场景演示

5.4.1.支持当前事务(required)

5.4.2.嵌套事务(nested)

--->PS:嵌套事务(附加事务)(NESTED)和加入事务(REQUIRED )的区别:

6.总结


1.为什么需要事务?

事务定义

将⼀组操作封装成⼀个执⾏单元(封装到⼀起),要么全部成功,要么全部失败。

为什么要⽤事务?

⽐如转账分为两个操作:第⼀步操作:A 账户 -100 元。第⼆步操作:B 账户 +100 元。

如果没有事务,第⼀步执⾏成功了,第⼆步执⾏失败了,那么 A 账户平⽩⽆故的 100 元就“⼈间蒸发”了。⽽如果使⽤事务就可以解决这个问题,让这⼀组操作要么⼀起成功,要么⼀起失败。

(当异常中断了,会将事务的执行过程记录到日志里,当台式电脑断电恢复之后,MySQL重新启动时会首先进行自检,发现某个事务执行了一半,没有结束符,此时会进行补偿机制,业务进行回滚,执行完上次没有执行完的操作,再提交事务。)

2.MySQL中事务的使用

事务在 MySQL 中有 3 个重要的操作:开启事务提交事务回滚事务,对应的操作命令如下:

--开启事务
start transaction;

--业务执行


--提交事务
commit;

--回滚事务
rollback;

MySQL中的事务都是单独执行的,不存在Spring中多个事务之间进行传递。

3.Spring中事务的实现

3.1.编程式事务(手动写代码操作事务)

灵活性大,但麻烦。

Spring ⼿动操作事务和上⾯ MySQL 操作事务类似,它也是有 3 个重要操作步骤:

  • ①开启事务(获取事务)
  • ②提交事务 或 ③回滚事务

SpringBoot 内置了两个对象,DataSourceTransactionManager ⽤来获取事务(开启事务)、提交或回滚事务的;TransactionDefinition 是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务TransactionStatus,实现代码如下:

# 配置数据库的连接字符串
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8  #utf8不支持一些复杂的中文
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver  #底层驱动的名称(8.0之前版本不加.cj,8.0之后版本加.cj)

# 配置mybatis xml的文件路径,在resource包下建立mybatis(其命名自定义)包,在resource/mybatis创建所有表的xml文件
mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml
  configuration:  # 配置打印MyBatis最终执行的SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 配置打印MyBatis最终执行的SQL
logging:
  level:
    com:
      example:
        demo: debug

保存起来,以后直接用就行。

创建model层下的实体类:

import lombok.Data;

@Data
public class UserInfo {
    private int id;
    private String username;
    private String password;
    private String photo;
    private String createtime;
    private String updatetime;
    private String state;
}

创建mapper层下的UserMapper接口写方法声明:

import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;

@Mapper //此注解一定不要忘记添加
public interface UserMapper {
    public int add(UserInfo userInfo);
}

创建mapper层下的UserMapper.xml文件写对应SQL标签:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
    <insert id="add">
        insert into userinfo(username, password)
        values(#{username}, #{password})
    </insert>
</mapper>

创建service层下的UserService类:

import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    public int add(UserInfo userInfo) {
        return userMapper.add(userInfo);
    }
}

创建controller层下的UserController类:

import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    //事务管理器
    @Autowired
    private DataSourceTransactionManager transactionManager;

    //事务属性对象
    @Autowired
    private TransactionDefinition transactionDefinition;

    @RequestMapping("/add")
    public int add(UserInfo userInfo) {
        //获取事务(开启事务)
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
        int result = userService.add(userInfo);
        log.info("添加结果:" + result);
//        //提交事务
//        transactionManager.commit(transactionStatus);
        //回滚事务
        transactionManager.rollback(transactionStatus);
        return result;
    }
}

事务回滚成功!若是提交事务,数据库会新增添加的数据。

以上代码虽然可以实现事务,但操作也很繁琐,更简单的实现⽅法——声明式事务。

3.2.声明式事务(利用注解自动开启和提交事务)(使用为主)

简单,但出现问题难解决。

声明式事务只需要在需要的⽅法上添加 @Transactional 注解就可以实现了,⽆需⼿动开启事务和提交事务,进⼊⽅法时自动开启事务,方法成功执行完会自动提交事务,如果中途发生了没有处理的异常会⾃动回滚事务。

(区别:单元测试里只要给要测试的方法上加上@Transactional注解,不管程序怎么执行,最终一定会回滚)

正常情况:

import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;
    
    @Transactional //开启声明式事务(进入方法自动开启事务,方法正常执行完自动提交事务,如果发生了未处理的异常会自动回滚事务)
    @RequestMapping("/add2")
    public int add2(UserInfo userInfo) {
        int result = userService.add(userInfo);
        log.info("添加功能II, 执行结果:" + result);
        return result;
    }
}

异常情况:

@Transactional 
@RequestMapping("/add2")
public int add2(UserInfo userInfo) {
    int result = userService.add(userInfo);
    log.info("添加功能II, 执行结果:" + result);
    int num = 10 / 0; //声明式事务执行到此行发生了未处理的异常会自动回滚事务
    return result;
}

 

3.2.1.@Transactional作用范围

@Transactional 可以⽤来修饰⽅法或类:

  • 修饰方法时:需要注意只能应用到 public 方法上,否则不生效。推荐此种⽤法。
  • 修饰时:表明该注解对该类中所有的 public ⽅法都⽣效。

3.2.2.@Transactional参数说明

timeout事务的超时时间:

 

 当遇到问题:

查看对应API文档。

3.2.3.注意事项:@Transactional + try-catch 有异常不回滚的问题

当(在异常被捕获的情况下) @Transactional 遇到try catch之后,即使程序执行一半出现了异常,那么事务也不会⾃动回滚。

原因——Spring设计理念:

  • 当程序有异常时,在没有加try catch时,Spring团队会认为当前的程序发生了一个始料未及的意外,开发者没有解决方案,会帮我们自动回滚;
  • 在加了try catch后,Spring团队会认为开发者预料到了代码可能会出现异常,认为开发者有责任和能力去解决这个异常,所以此时不会人为干预,会自动提交事务。
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/add2")
    public int add2(UserInfo userInfo) throws InterruptedException {
        int result = 0;
        try {
            result = userService.add(userInfo);
            log.info("添加功能II, 执行结果:" + result);
            int num = 10 / 0;
        } catch (Exception e) {
        }
        return result;
    }
}

解决方案1:对于捕获的异常将异常重新抛出,交给Spring处理,事务就会⾃动回滚了。对前端不友好。一般不用。

@Transactional
@RequestMapping("/add2")
public int add2(UserInfo userInfo) throws InterruptedException {
    int result = 0;
    try {
        result = userService.add(userInfo);
        log.info("添加功能II, 执行结果:" + result);
        int num = 10 / 0;
    } catch (Exception e) {
        throw e; //将异常重新抛出去
    }
    return result;
}

解决方案2手动回滚事务,在⽅法中使⽤ TransactionAspectSupport.currentTransactionStatus() 以得到当前的事务,然后设置回滚⽅法 setRollbackOnly 就可以实现回滚了。推荐使用。

import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/add2")
    public int add2(UserInfo userInfo) throws InterruptedException {
        int result = 0;
        try {
            result = userService.add(userInfo);
            log.info("添加功能II, 执行结果:" + result);
            int num = 10 / 0;
        } catch (Exception e) {
            log.info("程序执行出现异常:" + e.getMessage());
            //手动回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return result;
    }
}

3.2.4.@Transactional工作原理

@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。 @Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。

@Transactional 实现思路预览:

@Transactional 具体执⾏细节如下图所示:

4.事务隔离级别

4.1.事务特性

事务有4 ⼤特性(ACID):原⼦性、持久性、⼀致性、隔离性。

①原子性(Atomicity,或称不可分割性)

⼀个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执⾏过程中发⽣错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执⾏过⼀样。

②一致性(Consistency)

在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写⼊的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯作。例如转账操作,张三和李四账户各有50元,张三给李四转账20元,不管是转账之前还是转账之后,他们俩账户的总额一定是和开始一致的。

③隔离性(Isolation,又称独立性)

数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

④持久性(Durability)

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

而这 4 种特性中,只有隔离性(隔离级别)是可以设置的。

为什么要设置事务的隔离级别?

是⽤来保障多个并发事务执⾏更可控,更符合操作者预期的。

什么是可控?

⽐如近⼏年⽐较严重的新冠病毒,我们会把直接接触到确证病例的⼈员隔离到酒店,⽽把间接接触者(和直接接触着但未确诊的⼈)隔离在⾃⼰的家中,也就是针对不同的⼈群,采取不同的隔离级别,这种隔离⽅式就和事务的隔离级别类似,都是采取某种⾏动让某个事件变的“更可控”。而事务的隔离级别就是为了防止,其他的事务影响当前事务执行的一种策略。

4.2.MySQL 事务隔离级别有4

①READ UNCOMMITTED:读未提交(Read Uncommitted)

也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚, 因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读

②READ COMMITTED:读已提交(Read Committed)

也叫提交读,(是 Oracle 数据库的默认事务隔离级别)该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读(描述的是对同一数据的修改)。

③REPEATABLE READ:可重复读(Repeatable Read)

是 MySQL 数据库的默认事务隔离级别,它能确保同⼀事务多次查询的结果⼀致。但也会有新的问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这就叫幻读Phantom Read)(描述的是数据的增加和删除)。

④SERIALIZABLE:序列化(Serializable)

事务最⾼隔离级别,它会强制事务排序,使之不会发生冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多。

  • 脏读:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错误的。

  • 不可重复读:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。

  • 幻读:⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务有新增了⼀部分数据。

在数据库中通过以下 SQL 查询全局事务隔离级别当前连接的事务隔离级别

select @@global.tx_isolation,@@tx_isolation;

4.3.Spring事务隔离级别有5种

①Isolation.DEFAULT以连接的数据库的全局事务隔离级别为主。(default)

②Isolation.READ_UNCOMMITTED:读未提交(=MySQL中的读未提交)(read_uncommitted)

可以读取到未提交的事务,存在脏读。

③Isolation.READ_COMMITTED:读已提交(=MySQL中的读已提交)(read_committed)

只能读取到已经提交的事务,解决了脏读,存在不可重复读。

④Isolation.REPEATABLE_READ:可重复读(=MySQL中的可重复读)(repeatable_read)

解决了不可重复读,但存在幻读(MySQL默认级别)。

⑤Isolation.SERIALIZABLE:串行化(=MySQL中的串行化)(serializable)

可以解决所有并发问题,但性能太低。

相比于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个 Isolation.DEFAULT。

MySQL事务隔离级别Spring事务隔离级别的区别:

MySQL相当于面条厂商,提供了4种配料;Spring相当于饭店,新增了1种配料。饭店根据客户口味(具体业务)决定要用哪些配料,不用哪些配料。

4.4.Spring中设置事务隔离级别

只需要设置 @Transactional ⾥的 isolation 属性(枚举)即可:

查看源码:

 

5.Spring事务传播机制

5.1.事务传播机制是什么?

Spring 事务传播机制定义了多个包含了事务的方法(就相当于MySQL里的事务),相互调用时,事务是如何在这些方法间进行传递的。

5.2.为什么需要事务传播机制?

事务隔离级别是保证多个并发事务执⾏的可控性的(稳定性),⽽事务传播机制是保证“一个”事务多个调用方法间的可控性的(稳定性)。

例⼦:像新冠病毒⼀样,它有不同的隔离⽅式(酒店隔离&居家隔离),是为了保证疫情可控,然⽽在每个⼈的隔离过程中,会有很多个执⾏的环节,⽐如酒店隔离,需要负责⼈员运送、物品运送、 消杀原⽣活区域、定时核算检查和定时送餐等很多环节,而事务传播机制就是保证⼀个事务在传递过程中是可靠性的,回到本身案例中就是保证每个⼈在隔离的过程中可控的。

事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题:

而事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题:

路途越远出现问题的概率就越大。

5.3.事务传播机制有哪些?

Propagation.REQUIRED:默认的事务传播级别(required)

它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建⼀个新的事务。(A方法需要去调用B方法,此时B方法的行为:会判断A方法是否有事务,若存在事务,把当前的B方法加入到A方法中)

②Propagation.SUPPORTS(supports)

如果当前存在事务,则加⼊该事务;如果当前没有事务,则以非事务的方式继续运⾏。

③Propagation.MANDATORY(mandatory:强制性)

如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。

Propagation.REQUIRES_NEW(requires_new)

表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。

Propagation.NOT_SUPPORTED(not_supported)

以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。

⑥Propagation.NEVER(never)

以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。

⑦Propagation.NESTED(nested)

如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。

以上 7 种传播⾏为,可以根据是否⽀持当前事务分为以下 3 类:

以情侣关系为例来理解以上分类:

5.4.Spring事务传播机制使用和各种场景演示

怎样设置事务的传播机制?

只需要设置 @Transactional ⾥的 propagation属性(枚举)即可:

 支持多个参数设置:

5.4.1.支持当前事务(required)

创建日志的实体类LogInfo

import lombok.Data;

@Data
public class LogInfo {
    private int id;
    private String name;
    private String desc;
    private String createtime;
}

在日志表的数据持久化层mapper下创建LogMapper接口:

import com.example.demo.model.LogInfo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface LogMapper {
    int add(LogInfo logInfo);
}

创建LogMapper.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.LogMapper">
    <insert id="add">
        insert into loginfo(`name`,`desc`)
        values(#{name},#{desc})
    </insert>
</mapper>

创建LogService类:

import com.example.demo.mapper.LogMapper;
import com.example.demo.model.LogInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.REQUIRED) //默认的,设置不设置都一样
    public int add(LogInfo logInfo) {
        int num = 10 / 0;
        return logMapper.add(logInfo);
    }
}
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public int add(UserInfo userInfo) {
        return userMapper.add(userInfo);
    }
}
import com.example.demo.model.LogInfo;
import com.example.demo.model.UserInfo;
import com.example.demo.service.LogService;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @Autowired
    private LogService logService;

    @RequestMapping("/add3")
    @Transactional(propagation = Propagation.REQUIRED)
    public int add3(UserInfo userInfo) {
        //非空效验
        if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) || !StringUtils.hasLength(userInfo.getPassword())) {
            return -1; //非法参数
        }
        int result = 0;
        //添加用户
        int resultByUser = userService.add(userInfo);
        log.info("添加用户的结果:" + resultByUser);
        //添加日志
        LogInfo logInfo = new LogInfo();
        logInfo.setName("添加用户");
        logInfo.setDesc("用户信息:" + userInfo);
        int resultByLog = logService.add(logInfo);
        log.info("添加日志的结果:" + resultByLog);
        if(resultByUser == 1 && resultByLog == 1) result = 1;
        return result;
    }
}

执行结果:数据库没有插入任何数据。

执行流程:

  1. UserService类中的add方法正常执行完成。

  2. LogService类中的add方法发生异常,执行报错。

  3. UserController有事务为REQUIRED,则userService.add()和logService.add()这两个方法都会加入该事务REQUIRED,当LogService类中的add方法发生异常而未做try-catch处理时,会导致整个程序发生异常。UserController类中的add3方法为了求稳,会将整个事务都会回滚,结果是数据库中loginfo表和userinfo表都没有添加任何数据。【事务对错的执行机制是优于事务的传播机制的】

给LogService类中的add方法里加上try-catch:

import com.example.demo.mapper.LogMapper;
import com.example.demo.model.LogInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import javax.annotation.Resource;

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public int add(LogInfo logInfo) {
        int result = logMapper.add(logInfo);
        try{
            int num = 10 / 0;
        } catch (Exception e) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            result = 0;
        }
        return result;
    }
}

执行结果和上面一样。

执行流程:

  1. UserService类中的add方法正常执行完成。

  2. LogService类中的add方法发生异常,执行报错。

  3. UserController有事务为REQUIRED,则userService.add()和logService.add()这两个方法都会加入该事务REQUIRED,当LogService类中的add方法发生异常并做了try-catch处理时,一损俱损,一荣俱荣,整个事务都会回滚,结果是数据库中loginfo表和userinfo表都没有添加任何数据。

5.4.2.嵌套事务(nested)

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @Autowired
    private LogService logService;

    @RequestMapping("/add3")
    @Transactional(propagation = Propagation.NESTED)
    public int add3(UserInfo userInfo) {
        //非空效验
        if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) || !StringUtils.hasLength(userInfo.getPassword())) {
            return -1; //非法参数
        }
        int result = 0;
        //添加用户
        int resultByUser = userService.add(userInfo);
        log.info("添加用户的结果:" + resultByUser);
        //添加日志
        LogInfo logInfo = new LogInfo();
        logInfo.setName("添加用户");
        logInfo.setDesc("用户信息:" + userInfo);
        int resultByLog = logService.add(logInfo);
        log.info("添加日志的结果:" + resultByLog);
        if(resultByUser == 1 && resultByLog == 1) result = 1;
        return result;
    }
}
@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.NESTED)
    public int add(UserInfo userInfo) {
        return userMapper.add(userInfo);
    }
}
@Service
public class LogService {
    @Resource
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.NESTED) //默认的,设置不设置都一样
    public int add(LogInfo logInfo) {
        int num = 10 / 0;
        return logMapper.add(logInfo);
    }
}

执行结果:数据库中没有添加任何数据。

执行流程:

  1. UserService类中的add方法正常执行完成。

  2. LogService类中的add方法发生异常,执行报错。

  3. UserController没有事务,创建事务为NESTED,则userService.add()和logService.add()这两个方法的事务也是NESTED,当LogService类中的add方法发生异常而未做try-catch处理时,logService.add()作为UserController类中的add3方法中的一部分,导致大方法add3中的某一个节点出现异常,导致整个程序发生异常,事务为了求稳,会将整个大的事务都进行回滚,结果是数据库中loginfo表和userinfo表都没有添加任何数据。【即使是NESTED,也是将整个事务进行回滚,原因是当检测到事务里的方法出错时,是不看事务传播机制的,优先走了异常的回滚,而没有走事务的传播机制,事务对错的执行机制是优于事务的传播机制的。当事务没有报错时,某一个节点出现异常,是以事务传播机制为准的

给LogService类中的add方法里加上try-catch:

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.NESTED)
    public int add(LogInfo logInfo) {
        int result = logMapper.add(logInfo);
        try{
            int num = 10 / 0;
        } catch (Exception e) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            result = 0;
        }
        return result;
    }
}

执行结果:数据库中loginfo表没有添加数据,userinfo表添加了数据。

执行流程:

  1. UserService类中的add方法正常执行完成。

  2. LogService类中的add方法发生异常,执行报错。

  3. UserController没有事务,创建事务为NESTED,则userService.add()和logService.add()这两个方法的事务也是NESTED,当LogService类中的add方法发生异常并做了try-catch处理时,只需要将logService.add()进行回滚,整个事务本身没有进行回滚,所以userService.add()正常执行,结果是数据库中loginfo表没有添加数据,userinfo表添加了数据。

--->PS:嵌套事务(附加事务)(NESTED)和加入事务(REQUIRED )的区别

加入是让自己成为其中的一部分,如果自己出现问题,那么整个事务都会出现问题。

嵌套语义更浅,让自己尝试加进去,如果自己出现问题,就把嵌套进来的自己干掉,但是整个事务在去执行时是不影响的。

  • 整个事务如果全部执⾏成功,⼆者的结果是⼀样的。

  • 如果事务执⾏到⼀半失败了,那么加入事务整个事务会全部回滚,不能实现部分事务的回滚;⽽嵌套事务会局部回滚,不会影响上⼀个⽅法中执⾏的结果。

嵌套事务之所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念,嵌套事务进⼊之后相当于新建了⼀个保存点,⽽回滚时只回滚到当前保存点,因此之前的事务是不受影响的,这⼀点可以在 MySQL 的官⽅⽂档汇总找到相应的资料:

MySQL官方文档https://dev.mysql.com/doc/refman/5.7/en/savepoint.html

⽽ REQUIRED 是加⼊到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是嵌套事务和加⼊事务的区别。

6.总结

  1. 在 Spring 项⽬中使⽤事务,⽤两种⽅法⼿动操作和声明式⾃动提交,其中后者使⽤的最多,在⽅法上添加 @Transactional 就可以实现了。

  2. 设置事务的隔离级别 @Transactional(isolation = Isolation.SERIALIZABLE),Spring 中的事务隔离级别有 5 种。

  3. 设置事务的传播机制 @Transactional(propagation = Propagation.REQUIRED),Spring 中的事务传播级别有 7 种。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/339066.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JetpackCompose从入门到实战学习笔记8—ConstraintLayout的简单使用

JetpackCompose从入门到实战学习笔记8—ConstraintLayout的简单使用 1.简介&#xff1a; Compose 中的 ConstraintLayout ConstraintLayout 是一种布局&#xff0c;让您可以相对于屏幕上的其他可组合项来放置可组合项。它是一种实用的替代方案&#xff0c;可代替使用多个已嵌…

JVM垃圾回收机制GC理解

目录JVM垃圾回收分代收集如何识别垃圾引用计数法可达性分析法引用关系四种类型&#xff1a; 强、软、弱、虚强引用软引用 SoftReference弱引用 WeakReferenceWeakHashMap软引用与虚引用的使用场景虚引用与引用队列引用队列虚引用 PhantomReference垃圾回收算法引用计数复制 Cop…

06- 梯度下降(SGDRegressor) (机器学习)

梯度下降算法也是先构建误差值的函数, 通过求误差值函数的最小值来达到误差最小的目的, 不过梯度下降是先随机取值, 然后求函数在该点的导数, 如果导数为正, 下一次取值向反方向移动, 如果导数为负, 正向移动, 直到导数取值极小时, 认定误差达到一个可以接受的范围, 然后导出相…

解读手机拍照的各个参数(拍照时,上面会有6个符号)

1第一个符号是闪光灯符号&#xff0c;如下图所示。有四种模式&#xff0c; 手机的闪光灯分别为关闭、自动、开启和常亮四种状态。 关闭&#xff1a;就是在任何情况下都不会闪光 自动&#xff1a;由手机来判断此时的光线强弱&#xff0c;若手机测光认为光线太弱&#xff0c;则…

前端限制 git commit 提交格式

团队开发中&#xff0c;每个人 git commit 的习惯都不一样&#xff0c;这样不利于对更新日志的筛选&#xff0c;也可以防止同事跑路后&#xff0c;出现 bug 后&#xff0c;看不懂他当时提交的日志究竟是改了个 bug 还是新增了一个功能&#xff0c;影响开发效率。 这时候就需要…

springboot驾校报名系统 微信小程序

系统分为用户和管理员&#xff0c;驾校教练三个角色 用户微信小程序端的主要功能有&#xff1a; 1.用户注册和登陆系统 2.用户查看驾校教练信息 3.用户查看驾校信息 4.用户查看驾校的车辆信息 5.用户查看驾校考试信息&#xff0c;在线报名考试 6.用户可以在线预约驾校的教练 7.…

深度学习笔记--修改、增加和删除预训练模型的特定层

目录 1--前言 2--初始化 VGG16 模型 3--修改特定层 4--新增特定层 5--删除特定层 6--固定预训练模型的权重 7--综合应用 1--前言 基于 Pytorch&#xff0c;以 VGG16 模型为例&#xff1b; 2--初始化 VGG16 模型 from torchvision import models# 初始化模型 vgg16 mo…

Java 反射详解

一、反射 1、什么是反射 反射允许对成员变量、成员方法和构造器的信息进行编程访问。 补充:暴力反射,非public修饰需要打开权限 setAccessible(boolean) 2、反射的作用 利用反射创建的对象可以无视修饰符调用类里面的内容可以跟配置文件结合起来使用&#xff0c;把要创建的对象…

【Jest】Jest单元测试环境搭建

文章目录前言1、项目环境搭建初始化仓库安装ts环境安装jest环境2、初始化项目文件夹前言 今天开始&#xff01;&#xff01;&#xff01;学习vue源码&#xff0c;那么要学习源码前首先要了解Jest。 https://www.jestjs.cn/ 官网自带中文非常友好&#xff01; 1、项目环境搭建…

【C++面试问答】搞清楚深拷贝与浅拷贝的区别

问题 深拷贝和浅拷贝的区别是面试中的常见问题之一&#xff0c;对于不同的编程语言&#xff0c;这个问题的回答可能稍有差别&#xff0c;下面我们就来探索一下它们之间的异同吧。 先来看看在JavaScript对象的深拷贝与浅拷贝的区别&#xff1a; 浅拷贝&#xff1a;只是复制了…

Postgresql 根据单列或几列分组去重row_number() over() partition by

Postgresql 根据单列或几列分组去重row_number() over() partition by 一般用于单列或者几列需要去重后进行计算值的 count(distinct(eid)) 可以 比如有个例子&#xff0c;需要根据名称&#xff0c;城市去筛选覆盖的道路长度&#xff0c;以月因为建立了唯一索引是ok的&#…

前端项目集成Vite配置一览无余

Vite配置文件 默认指定&#xff1a;vite.config.js。自定义指定&#xff1a;vite --config 自定义名称.js。 Vite相关命令 查看Vite有哪些命令&#xff1a;npx vite -help。 --host [host]// 指定域名 --port <port>// 指定端口 --https // 使用 TLSHTTP/2 --cors //…

Spring 中 ApplicationContext 和 BeanFactory 的区别

文章目录类图包目录不同国际化强大的事件机制&#xff08;Event&#xff09;底层资源的访问延迟加载常用容器类图 包目录不同 spring-beans.jar 中 org.springframework.beans.factory.BeanFactoryspring-context.jar 中 org.springframework.context.ApplicationContext 国际…

HTTP 和 HTTPS 的区别

文章目录前言一、HTTP 与 HTTPS 的基本概念HTTPHTTPS二、HTTP 和 HTTPS协议的区别前言 浏览网站时&#xff0c;我们会发现网址有两种格式&#xff0c;一种以http://开头&#xff0c;一种https://开头。好像这两种格式差别不大&#xff0c;只多了一个s&#xff0c;实际上他们有…

Java零基础教程——数组

目录数组静态初始化数组数组的访问数组的动态初始化元素默认值规则&#xff1a;数组的遍历数组遍历-求和冒泡排序数组的逆序交换数组 数组就是用来存储一批同种类型数据的容器。 20, 10, 80, 60, 90 int[] arr {20, 10, 80, 60, 90}; //位置 0 1 2 3 4数组的…

死锁的原因及解决方法

❣️关注专栏&#xff1a; JavaEE 死锁☘️1.什么是死锁☘️2.死锁的三个典型情况☘️2.1情况一☘️2.2情况二☘️2.2.1死锁的代码展示☘️2.3多个线程多把锁☘️3死锁产生的必要条件☘️3.1互斥性☘️3.2不可抢占☘️3.3请求和保持☘️3.4循环等待☘️4如何避免死锁☘️4.1避免…

【Spark分布式内存计算框架——Spark Core】6. RDD 持久化

3.6 RDD 持久化 在实际开发中某些RDD的计算或转换可能会比较耗费时间&#xff0c;如果这些RDD后续还会频繁的被使用到&#xff0c;那么可以将这些RDD进行持久化/缓存&#xff0c;这样下次再使用到的时候就不用再重新计算了&#xff0c;提高了程序运行的效率。 缓存函数 可以…

Kubernetes集群-部署Java项目

Kubernetes集群-部署Java项目&#xff08;SSG&#xff09; k8s部署项目java流程图 第一步 打包制作镜像 打包 java源码&#xff1a; application.properties #在有pom.xml的路径下执行 mvn clean package制作镜像&#xff1a; 将刚才打包后的文件夹传到&#xff0c;装有dock…

(考研湖科大教书匠计算机网络)第三章数据链路层-第十一节:虚拟局域网VLAN概述和实现机制

获取pdf&#xff1a;密码7281专栏目录首页&#xff1a;【专栏必读】考研湖科大教书匠计算机网络笔记导航 文章目录一&#xff1a;VLAN概述&#xff08;1&#xff09;分割广播域&#xff08;2&#xff09;虚拟局域网VLAN二&#xff1a;VLAN实现机制&#xff08;1&#xff09;IEE…

LeetCode题目笔记——15. 三数之和

文章目录题目描述题目链接题目难度——中等方法一&#xff1a;暴力&#xff08;参考&#xff09;代码/Python 参考方法二&#xff1a;哈希代码/Python参考方法三&#xff1a;排序双指针代码/Python代码/C总结题目描述 龙门阵&#xff1a;这个n个数之和是不是有什么深意啊&#…