场景引入:

在程序开发的时候,我们会遇到这样的一个场景:转账

A转账给B

A账户:-100(扣款)

B账户:金额不变(未到账)

未到账原因可能是数据库断开连接,主机崩溃等等。

此时,如若不做其他处理,那么A就是白白丢失了这100块钱

所以,为了解决这个问题,那么就引入事务了。

事务:

对于事务这块,小编的的MySQL专栏中的事务文章,做了相对详细的分享,有兴趣的可以回头看看。

那么在这里呢,就简单解释下

事务是一组操作的集合,是一个不可分割的操作。

事务会把所有的操作作为一个整体,一起向数据库提交或是撤销数据库请求,所以这组操作要么同时完成,要么同时失败。

事务操作(MySQL)

在MySQL中,事务的操作主要是分为三步

1.开启事务(一组操作前开启事务)

2.提交事务(这组操作全部成功,提交事务)

3.回滚事务(这组操作中间任何一个操作出现异常,回滚事务)

--开启事务
start transaction;
--提交事务
commit;
--回滚事务
rollback;

以上是展示MySQL如何操作的

以下是在Spring中展示Spring如何操作

事务操作(Spring)

一:项目准备

1.创建一个Springboot项目

2.创建以下表:

-- 创建数据库
 DROP DATABASE IF EXISTS trans_test;
 CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;

 --用户表
 DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
    `id` INT NOT NULL AUTO_INCREMENT,
    `user_name` VARCHAR(128) NOT NULL,
    `password` VARCHAR(128) NOT NULL,
    `create_time` DATETIME DEFAULT now(),
    `update_time` DATETIME DEFAULT now() ON UPDATE now(),
    PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '用户表';

-- 操作日志表
-- DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info (
    `id` INT PRIMARY KEY auto_increment,
    `user_name` VARCHAR(128) NOT NULL,
    `op` VARCHAR(256) NOT NULL,
    `create_time` DATETIME DEFAULT now(),
    `update_time` DATETIME DEFAULT now() ON UPDATE now()
) DEFAULT charset 'utf8mb4';

3.创建model包,并创建以下类

这两个类是暂时没有用到。

@Data
public class LogInfo {
    private int id;
    private String userName;
    private String op;
    private Date   createTime;
    private Date   updateTime;
}
@Data
public class UserInfo {
    private int id;
    private String userName;
    private String password;
    private Date   createTime;
    private Date   updateTime;
}

4.创建mapper层,并创建以下类

@Mapper
public interface LogInfoMapper {
    @Insert("insert into log_info(user_name,op) " +
            "values(#{userName},#{op})")
    Integer createLogInfo(String userName,String op);

}
@Mapper
public interface UserInfoMapper {
    @Insert("insert into user_info(user_name,password) values" +
            "(#{userName},#{password})")
    Integer register(String userName,String password);
}

5.创建Service层,并创建以下类

@Service
public class UserInfoService {
    @Autowired
    private UserInfoMapper mapper;
    public int insert(String username,String password){
       return mapper.register(username,password);
    }
}
@Service
public class LogInfoService {
    @Autowired
    private LogInfoMapper mapper;
    public int insertLog(String username, String op) {
    Integer ret = mapper.createLogInfo(username, op);
    return ret;
    }
}

6.创建controller层,并创建以下类

@RestController
@RequestMapping("/log")
public class LogInfoController {
    @Autowired
    private LogInfoService service;
    public String insertLog(String username,String op){
        service.insertLog(username,op);
        return "插入日志成功!";
    }
}

二:演示事务

在spring中,提供了三种方式供我们使用事务使用

1.编程式事务

2.基于注解的声明式事务

3.基于XML的声明式事务

本文演示前两种,第三种有兴趣可以网上再去看看。

编程式事务:

在controller层,创建以下类:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserInfoController {
    @Autowired
    private UserInfoService service;

    //以下使用代码方式来提交事务
    //获取一个事务管理对象
    @Autowired
    private DataSourceTransactionManager manager;
    //获取一个事务定义
    @Autowired
    private TransactionDefinition definition;

    @RequestMapping("u1")
    public String register(String username,String password){

        //开启事务
        TransactionStatus status=manager.getTransaction(definition);

        service.insert(username,password);
        //提交事务
        manager.commit(status);

        return "注册成功!";
    }
}

DataSourceTransactionManager 、TransactionDefinition 这两个对象,Spring帮我们管理好了

直接注入即可

创建一个管理对象以及一个事务定义,是为了显式去控制事务

允许程序通过postman测试接口

结果返回:

值得注意的是,如若发生异常,即代码中有运行时异常,那么此时事务就会提交失败

那么出现失败的话,此时我们应该进行回滚,下面演示出错了如何回滚

@Slf4j
@RestController
@RequestMapping("/user")
public class UserInfoController {
    @Autowired
    private UserInfoService service;

    //以下使用代码方式来提交事务
    //获取一个事务管理对象
    @Autowired
    private DataSourceTransactionManager manager;
    //获取一个事务定义
    @Autowired
    private TransactionDefinition definition;


    @RequestMapping("u1")
    public String register(String username,String password){

        //开启事务
        TransactionStatus status=manager.getTransaction(definition);

        service.insert(username,password);
        try{
            int ret=10/0;
        }catch (Exception e){
            log.info("插入出错!");
            //事务回滚
            manager.rollback(status);
        }
        //提交事务
        manager.commit(status);

        return "注册成功!";
    }
}

通过postman测试同样接口,返回结果如下:

数据库并没有数据,但插入语句确实执行了,那么在navicat中右键user_info的设计表中可以看到此结果

对于编程式事务,就分享到这里

基于注解式事务

在controller包中创建该TransactionController类

@Transactional注解介绍:

它是 Spring 提供的用于 声明式事务管理 的注解,用于在方法或类上标注,让 Spring 自动在方法执行前开启事务,在方法执行成功后提交事务,如果方法抛出异常,则回滚事务。

表格 还在加载中,请等待加载完成后再尝试复制

使用注解(无异常):
@Slf4j
@RestController
@RequestMapping("/user2")
public class TransactionController {
 @Autowired
private UserInfoService service;
//以下使用注解来使用事务

/**
 * 情况一:使用注解,无异常情况下,数据插入成功
 * @param username
 * @param password
 * @return
 */
@Transactional
@RequestMapping("/u1")
public String r1(String username,String password){
    service.insert(username,password);
    return "注册成功!";
}
  }

访问该接口,这里及其后面给出url:http://127.0.0.1:8080/user2/u1,不给postman截图

使用注解(异常)
/**
 * 添加事务(代码出错),事务自动回滚
 * @param username
 * @param password
 * @return
 */
@Transactional
@RequestMapping("/u2")
public String r2(String username,String password){
    int ret = service.insert(username, password);
    if(ret==1){
        log.info("插入成功,影响行数:"+ret);
    }
    int temp=10/0;
    return "注册成功!";
}

此时,当代码出现运行时异常的时候,该注解回滚代码

访问url:http://127.0.0.1:8080/user2/u2

此时插入代码执行了,所以该表ID也是自动增加了

使用catch语句处理
/**
 * 出错语句被catch住,那么事务不会回滚
 * @param username
 * @param password
 * @return
 */
@Transactional
@RequestMapping("/u3")
public String r3(String username,String password){
    int ret = service.insert(username, password);
    if(ret==1){
        log.info("插入成功,影响行数:"+ret);
    }
    try {
        int temp=10/0;
    } catch (Exception e) {
        log.info("运行出错,事务不回滚");
    }
    return "注册成功!";
}

访问URL:http://127.0.0.1:8080/user2/u3

使用catch语句(仍抛出异常)
/**
 * 事务被catch住,但抛出后运行时异常,事务回滚
 * @param username
 * @param password
 * @return
 */
@Transactional
@RequestMapping("/u4")
public String r4(String username,String password){
    int ret = service.insert(username, password);
    if(ret==1){
        log.info("插入成功,影响行数:"+ret);
    }
    try {
        int temp=10/0;
    } catch (Exception e) {
        log.info("运行出错,事务不回滚");
        throw e;
    }
    return "注册成功!";
}

访问URL:http://127.0.0.1:8080/user2/u4

按照顺序而言,此时ID自增为6,在执行玩插入代码后

catch也不是限定死的,即使是被catch住,也可以进行事务强制回滚

使用catch语句(强制回滚)
/**
 * 既然抛出异常了,catch住,仍然可以设置回滚
 * @param username
 * @param password
 * @return
 */
@Transactional
@RequestMapping("/u5")
public String r5(String username,String password){
    int ret = service.insert(username, password);
    if(ret==1){
        log.info("插入成功,影响行数:"+ret);
    }
    try {
        int temp=10/0;
    } catch (Exception e) {
        log.info("运行出错,事务不回滚");

        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return "注册成功!";
}

访问URL:http://127.0.0.1:8080/user2/u5

此时,执行完插入语句后,该ID自增为7

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

在当前事务中手动标记该事务只能回滚,即使没有抛出异常,Spring 也会在方法执行完后回滚事务,而不是提交。

必须在 事务方法内部调用才有效,否则会抛出 NoTransactionException

不能用于非事务方法或非事务线程中。

抛出非运行时异常
/**
 *抛出非运行时异常,事务仍然会提交
 * @param username
 * @param password
 * @return
 */
@Transactional
@RequestMapping("/u6")
public String r6(String username,String password) throws IOException {
    int ret = service.insert(username, password);
    if(ret==1){
        log.info("插入成功,影响行数:"+ret);
    }
    if(true){
        throw new IOException();
    }
    return "注册成功!";
}

所以说明的是,该事务注解出现异常回滚时是对运行时异常有效

对于非运行时异常而言,也可以通过该设置@Transactional注解中的rollbackFor值来回滚事务

抛出非运行时异常(设置回滚)
/**
 * 通过设置rollbackFor,非运行时异常也可以回滚
 * @param username
 * @param password
 * @return
 * @throws IOException
 */
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/u7")
public String r7(String username,String password) throws IOException {
    int ret = service.insert(username, password);
    if(ret==1){
        log.info("插入成功,影响行数:"+ret);
    }
    if(true){
        throw new IOException();
    }
    return "注册成功!";
}

访问URL:http://127.0.0.1:8080/user2/u7

此时ID自增为9

只要代码抛出的异常属于rollbackFor = Exception.class中的值的子类活着是本身,都可以进行回滚

如若是rollbackFor = ?设置的值是代码抛出异常的子类,即rollbackFor = ?是范围小,代码抛出的异常是范围大

那么情况又是如何?

抛出非运行时异常(设定范围)
/**
 * 在非抛出非运行时异常的情况下,设定小范围异常,抛出大范围异常
 * 事务不回滚
 * @param username
 * @param password
 * @return
 * @throws IOException
 */
@Transactional(rollbackFor = JSException.class)
@RequestMapping("/u8")
public String r8(String username,String password) throws IOException {
    int ret = service.insert(username, password);
    if(ret==1){
        log.info("插入成功,影响行数:"+ret);
    }
    if(true){
        throw new IOException();
    }
    return "注册成功!";
}

访问URL:http://127.0.0.1:8080/user2/u8

设定事务的隔离级别

对于事务的隔离级别相对详细解释,请看这个小编另一个文章:https://blog.csdn.net/m0_75169015/article/details/143476687

那么对应到Spring中的事务隔离级别,有以下:

Isolaolation.DEFAULT:以连接的数据的事务隔离级别为主(MySQL为读已提交)

Isolaolation.READ_UNCOMMTIED:读未提交,对应SQL标准中READ UNCOMMTIED

Isolaolation.READ_COMMTIED:读已提交,对应SQL标准中READ COMMTIED

Isolaolation.REPEATABLE_READ:可重复读,对应SQL标准中REPEATABLE READ

Isolaolation.SERIALIZABLE:串行化,对应SQL标准中SERIALIZABLE

举例代码,在TransactionController类中,插入以下代码片段:

  /**
     * 设置事务的隔离级别,mysql默认是读已提交
     * @param username
     * @param password
     * @return
     * @throws IOException
     */
    @Transactional(isolation = Isolation.DEFAULT)
    @RequestMapping("/u9")
    public String r9(String username,String password) throws IOException {
        int ret = service.insert(username, password);
        if(ret==1){
            log.info("插入成功,影响行数:"+ret);
        }
        if(true){
            throw new IOException();
        }
        return "注册成功!";
    }
}

访问URL:http://127.0.0.1:8080/user2/u9

以上呢,是对你会发现有些是事务失效了,有些是事务生效了,那么对于事务失效这块而言,原因上面提到的

是不多的,下面就给出对于事务失效常见原因

事务失效:

1.方法不是public

Spring AOP 默认只拦截 public 方法,protected/private 的方法不会被代理。

2.内部方法调用(this调用自己)

一个带 @Transactional 的方法如果被同类的另一个方法直接调用,是不会经过代理对象的,事务不会生效。

因为,这个@Transactional是基于SpringAOP实现的,那么对于SpringAOP只会对生成代理对象调用的方法进行增强

并不会拦截内部this调用的方法

代码示例:

public void outer() {
    this.inner(); // 不生效
}

@Transactional
public void inner() {
    // 不会被代理拦截
}

解决办法:

通过代理对象调用对应方法

@Service
public class UserService {

    @Autowired
    private UserService self; // 注意:Spring 会注入代理对象

    public void methodA() {
        self.methodB(); //  调用的是代理对象的方法,事务生效!
    }

    @Transactional
    public void methodB() {
        ...
    }
}

抽取到另一个类中调用

@Service
public class InnerService {

    @Transactional
    public void methodB() {
        ...
    }
}

@Service
public class UserService {

    @Autowired
    private InnerService inner;

    public void methodA() {
        inner.methodB(); //  生效!
    }
}

3.异常被catch掉,没有手动回滚

spring事务默认只对方法中抛出未捕获的RuntimeException才会回滚

具体代码,演示事务中已体现

4. 抛出的不是 RuntimeException 且未设置 rollbackFor

5.多线程/异步操作中事务失效

代码举例:

@Transactional
public void asyncSave() {
    new Thread(() -> {
        repository.save(...); //  无事务保护
    }).start();
}

这样是失效的,原因简单解释:

Spring 的事务管理是基于 ThreadLocal 的。

ThreadLocal 是什么?

它是一个线程私有的变量,Spring 会这样维护事务:

// 概念性伪代码
ThreadLocal<TransactionStatus> threadLocal = new ThreadLocal<>();

事务状态是绑定在当前线程上的,新线程不会共享老线程的 ThreadLocal 值。

所以你开启一个新线程后,Spring 就再也找不到原来的事务上下文,自然也就无法参与事务管理。

解决方法:

1.不手动开启线程,用@Async

@Service
public class UserService {

    @Async
    @Transactional
    public void asyncSave() {
        repository.save(...); //  事务生效
    }
}
@SpringBootApplication
@EnableAsync
public class MyApp {
}

使用 @Async 会让 Spring 管理线程池;

Spring 能在这个线程中创建自己的代理对象(有事务、有上下文);

所以 @Transactional 可以生效。

2.在线程内部使用编程式事务

3.把对应操作放进主线程中执行

事务传播机制

用于定义 当一个事务方法调用另一个事务方法时,事务该如何传播(处理)

简单而言,就是A方法有事务,B方法是有事务,A调用B时,B事务是加入A中,还是新建一个事务。

那么这个事务传播机制是通过@Transactional中的propagation属性设置的。

A调用B,已B为基准

事务传播机制种类:

  1. Propagation.REQUIRED

默认的事务传播机制,如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新事务

  1. Propagation.SUPPORTS

如若当前存在事务,则加入该事务,如若当前没有事务,则以非事务方式运行

  1. Propagation.MANDATORY

强制性,如若当前存在事务,则加入该事务,如若当前没有事务,则抛出异常

  1. Propagation.REQUIRES_NEW

创建一个新的事务,如若当前存在事务,则把当前事务挂起,也就是说不管外部方法事务是否开启,当前方法设置该属性后,该方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。

  1. Propagation.NOT_SUPPORTS

以非事务的方式运行,如果当前存在事务,则把当前事务挂起

  1. Propagation.NEVER

以非事务方式运行,如若存在事务,则抛出异常

  1. Propagation.NESTED

如果当前存在事务,则创建一个事务作为当前事务来运行,如若没有事务,该取值等同Propagation.REQUIRED

下面通过代码演示四个情况:

情况皆以A当前存在事务,B也存在事务

代码准备:

controller层创建该类

@RestController
@RequestMapping("/propagation")
public class propagationController {
    @Autowired
    private LogInfoService logInfoService;
    @Autowired
    private UserInfoService userInfoService;

    //以下演示为事务传播机制
    //controller加事务是不合规范的
    @Transactional
    @RequestMapping("/p1")
    public String p1(String username,String password){

        userInfoService.insert(username,password);
        try {
            logInfoService.insertLog(username,"用户注册");
        }catch (Exception e){
            System.out.println("子事务异常被捕获"+e.getMessage());
        }

        return "注册/日志插入成功";
    }

}

service层修改UserInfoService

@Service
public class UserInfoService {
    @Autowired
    private UserInfoMapper mapper;
    @Transactional
    public int insert(String username,String password){
       return mapper.register(username,password);
    }
}
Propagation.REQUIRED

在service层中的修改该类,内容修改后如下

@Service
public class LogInfoService {
    @Autowired
    private LogInfoMapper mapper;

    //演示事务传播机制Propagation.REQUIRED
    @Transactional(propagation = Propagation.REQUIRED)
    public int insertLog(String username,String op){
        return mapper.createLogInfo(username, op);
    }
}    

访问URL:http://127.0.0.1:8080/propagation/p1,依旧通过postman测试

Propagation.REQUIRES_NEW

该LogInfoService 类下添加:

//演示事务传播机制Propagation.REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int insertLog(String username, String op) {

    Integer ret = mapper.createLogInfo(username, op);
    int temp = 10 / 0;
    return ret;
}

访问URL:http://127.0.0.1:8080/propagation/p1

由于是logservice中的@Transactional是设置了该Propagation.REQUIRES_NEW

所以开启了一个新事务,新事务与其他事务独立,此时logservice遇到异常了,所以只会是logservice中事务回滚,不涉及到userservice

也不涉及到propagationController中回滚

Propagation.NEVER
    //演示事务传播机制Propagation.NEVER
    @Transactional(propagation = Propagation.NEVER)
    public int insertLog(String username, String op) {
        Integer ret = mapper.createLogInfo(username, op);
        return ret;
    }

访问URL:http://127.0.0.1:8080/propagation/p1

显然此时发现控制台中,只有用户注册成功,日志记录没有插入成功。

Propagation.NESTED
//演示事务传播机制Propagation.NESTED
@Transactional(propagation = Propagation.NESTED)
public int insertLog(String username, String op) {

    Integer ret = mapper.createLogInfo(username, op);
    int temp = 10 / 0;
    return ret;
}

访问URL:http://127.0.0.1:8080/propagation/p1

此时由于设置了机制为NESTED,所以回滚的也只是logservice中方法,主事务即controller中的方法

并不会回滚。

那么如若propagationController没有把异常进行捕获,此时主事务和嵌套事务将会一起回滚。

对于NESTED中,嵌套事务是如何回滚的,这里简单说下

NESTED 的本质:使用保存点 Savepoint 回滚子流程

只要主事务还没提交,你可以把某一部分失败的子逻辑 “撤回” 到某个保存点,而不影响主流程。

就像游戏里你打副本,存了个档,发现这一波走错了,load 回保存点再继续打。

以上就是小编分享Spring中事务一些简单事项

文章作者: 南汐
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 www.phblog.cloud
JavaFrame JavaFrame
喜欢就支持一下吧