Spring -- 事务

Spring中事务的操作分为两类:

(1)编程式事务 -- 手动写代码操作事务

(2)声明式事务 -- 利用注解开启事务和提交事务

1. 编程式事务

准备Controller

java 复制代码
@RestController
@RequestMapping("/user")
public class UserInfoController {
    @Autowired
    private UserInfoService userInfoService;
    @RequestMapping("addUser")
    public String addUser(UserInfo userInfo) {
        userInfoService.insert(userInfo);
        return "SUCCESS";
    }
}

Spring Boot内置了两个对象
DataSourceTransactionManager事务管理器,用来开始事务,提交或回滚事务
TransactionDefinition是事务的属性,在开启事务的时候需要将TransactionDefinition传递进去从而获得一个事务TransactionStatus

先来看没有事务的执行结果:

访问后:

如果设置了事务:

java 复制代码
@RestController
@RequestMapping("/user")
@Slf4j
public class UserInfoController {
    //事务管理器
    @Autowired
    private DataSourceTransactionManager transactionManager;

    //定义事务属性
    @Autowired
    private TransactionDefinition transactionDefinition;


    @Autowired
    private UserInfoService userInfoService;

    @RequestMapping("/addUser")
    public String addUser(UserInfo userInfo) {
        log.info("addUser:{}",userInfo);
        //开启事务
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

        userInfoService.insert(userInfo);

        //事务提交
        transactionManager.commit(transactionStatus);
        return "SUCCESS";
    }
}

执行结果:

观察数据库,发现两条数据都有插入:

如果我们设置了回滚

java 复制代码
@RequestMapping("/addUser")
public String addUser(UserInfo userInfo) {
log.info("addUser:{}",userInfo);
//开启事务
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

userInfoService.insert(userInfo);

//事务提交
transactionManager.rollback(transactionStatus);
return "SUCCESS";
}

对比发现,如果是成功提交,输出的日志是

如果回滚了,输出的日志是没有这一条信息的,并且数据库也不会更新

但是如果此时我们再次提交事务:

查看数据库的数据:

中间空着的id为17的数据就是我们刚刚回滚的操作

2. Spring声明式事务@Transactional

声明式事务的实现需要两步:

2.1 添加依赖

xml 复制代码
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency> 

2.2 添加@Transactional注解

在需要添加事务的地方加上@Transactional注解即可,无需手动开启事务和提交事务
如果中途发生了没有处理的异常会回滚事务

正常情况:

java 复制代码
@RestController
@RequestMapping("/user1")
@Slf4j
public class UserInfoController1 {
    @Autowired
    private UserInfoService userInfoService;

    @Transactional
    @RequestMapping("/addUser1")
    public String addUser(UserInfo userInfo) {
        log.info("addUser:{}",userInfo);
        userInfoService.insert(userInfo);
        return "SUCCESS";
    }
}

异常情况:

java 复制代码
@Transactional
@RequestMapping("/addUser1")
public String addUser(UserInfo userInfo) {
log.info("addUser:{}",userInfo);

userInfoService.insert(userInfo);
int a = 10/0;//出现未处理的异常
return "SUCCESS";
}

此时会自动回滚,没有提交

但是如果异常在方法内被处理了,就会提交事务

java 复制代码
@Transactional
@RequestMapping("/addUser1")
public String addUser(UserInfo userInfo) {
log.info("addUser:{}",userInfo);

userInfoService.insert(userInfo);
try {
    int a = 10/0;
} catch (Exception e) {
    e.printStackTrace();
}
return "SUCCESS";
}

我们可以在catch里面手动进行回滚事务:

java 复制代码
@Transactional
@RequestMapping("/addUser1")
public String addUser(UserInfo userInfo) {
log.info("addUser:{}",userInfo);

userInfoService.insert(userInfo);
try {
    int a = 10/0;
} catch (Exception e) {
    e.printStackTrace();
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "SUCCESS";
}

再次访问:不会提交

注意:@**Transactional**** **注解可以用来修饰方法或类:

  • 修饰方法时,只有修饰public方法时才生效(修饰其他方法时不会报错,但是不会生效)
  • 修饰类时,类中所有的public方法都会生效
java 复制代码
@Transactional
@RequestMapping("/addUser1")
public String addUser(UserInfo userInfo) {
log.info("addUser:{}",userInfo);

userInfoService.insert(userInfo);
try {
    int a = 10/0;
} catch (Exception e) {
    e.printStackTrace();
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "SUCCESS";
}

2.3 @Transactional详解

@Transactional有三个重要的属性:

2.3.1 rollbackFor

@Transactional默认只是在遇到运行时异常以及Error时才会回滚,非运行时异常不会回滚

java 复制代码
@Transactional
@RequestMapping("/addUser1")
public String addUser(UserInfo userInfo) throws IOException {
    log.info("addUser:{}",userInfo);

    userInfoService.insert(userInfo);
    if (true) {
        throw new IOException();
    }
    return "SUCCESS";
}

访问:

事务是会提交的

如果我们需要对所有的异常都回滚,需要配置@Transactional注解当中的rollbackFor属性指定出现何种异常时事务进行回滚

@Transactional(rollbackFor = Exception.class)
@RequestMapping("/addUser1")
public String addUser(UserInfo userInfo) throws IOException {
    log.info("addUser:{}",userInfo);

    userInfoService.insert(userInfo);
    if (true) {
        throw new IOException();
    }
    return "SUCCESS";
}

再次访问:事务不会提交

2.3.2 事务隔离级别
MySQL事务隔离级别

SQL定义了四种隔离级别:

  1. 读未提交:该隔离级别的事务可以看到其他事务中未提交的数据

其他事务未提交的数据可能会发生回滚,但是该隔离级别的事务可以读到,我们吧该级别读到的数据称为脏数据,这个问题称为脏读

  1. 读已提交:该隔离级别的事务能读取到已经提交事务的数据

该隔离级别不会有脏读的问题,但是由于在事务的执行过程中可以读取到其他事务提交的结果,所以在不同时间相同的sql查询可能会读到不同的结果,这种现象叫做不可重复读

  1. 可重复读:事务不会读到其他事务对已有数据的修改,即使其他事务已经提交,也可以确保

在同一事务多次查询的结果是一致的,但是其他事务新插入的数据,是可以感知到的,这也就引发了幻读问题.可重复读是MySQL默认的事务隔离级别

  1. 串行化:序列化,事务最高的隔离级别,他会强制事务排序,使之不会产生冲突,从而解决

脏读,不可重复读和幻读问题,但是执行效率低,使用场景不多

Spring事务隔离级别

Spring中事务隔离级别有5种

  1. Isolatio.DEFAULT以连接的数据库的事务隔离级别为主
  2. Isolation.READ_UNCOMMITTED:读未提交,对应SQL标准中的读未提交
  3. Isolation.READ_COMMITTED: 读已提交,对应SQL标准中的读已提交
  4. Isolation.REPEATABLE_READ:可重复读.对应SQL标准中的可重复读
  5. Isolation.SERIALIZABLE:串行化,对应SQL标准中的串行化

Spring中事务隔离级别可以通过@Transactional中的Isolation属性进行设置

java 复制代码
@Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_UNCOMMITTED)
@RequestMapping("/addUser1")
public String addUser(UserInfo userInfo) throws IOException {
    log.info("addUser:{}",userInfo);

    userInfoService.insert(userInfo);

    return "SUCCESS";
}
2.3.3 事务传播机制

事务传播机制就是,多个事务之间存在调用关系时,事务是如何在这些方法间进行传播的

比如A方法调用B方法,A执行时,会开启一个事务,B本身也存在一个事务,此时B的事务是加入A的事务,还是创建一个新的事务

这就涉及到了事务传播机制
@Transactional注解支持事务传播机制的设置,通过propagation属性来指定传播行为

  1. Propagation.REQUIRED:默认的事务传播级别.表示如果当前存在事务,则加入该事务.如果当前没有事务,则创建一个事务
  2. Propagation.SUPPORTS:如果当前存在事务,则加入事务;如果当前没有存在事务,则以非事务的方式继续进行
  3. Propagation.MANDATORY:强制性,如果当前存在事务,则加入事务,如果当前没有事务,则抛出异常
  4. Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起,也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法都会新开自己的事务,且开启的事务相互独立,互不干扰
  5. Propagation.NOT_SUPPORTED以非事务的方式运行,如果当前存在事务,则把当前的事务挂起
  6. Propagation.NEVER以非事务的方式运行,如果当前存在事务,则抛出异常
  7. Propagation.NESTED如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果没有事务,则该取值等价于Propagation.REQUIRED

我们通过代码来演示一下区别:

(1)REQUIRED

此时由于是REQUIRED,那么三个方法针对的是同一个事务,只要其中有一个出现异常,都会回滚

(2)REQUIRES_NEW

在这种条件下,事务之间是独立的,因此互不影响

此时user的事务提交了,但是log的事务回滚了

(3)NEVER

如果存在事务,那就抛出异常

(4) NESTED

我们先来看看REQUIRED 如果方法都执行成功的情况下:

如果是NESTED,方法都执行成功的情况下:


可以发现是等价的,同样是只是开启了一次事务

如果发生了异常

对于REQUIRED方法:

如果是NESTED

事务都是全部回滚

看似两种好像都是一样的,实际上存在差别
REQUIRED是加入,而NESTED是嵌套

我们通过以下代码就能很好的看出两种的区别:

对于NESTEDE:

但是对于REQUIRED:

数据库两个表都没有新增数据

可以注意到,在REQUIRED里面,由于是同一个事务,因此不能部分回滚

相关推荐
阿华的代码王国6 分钟前
MySQL ------- 索引(B树B+树)
数据库·mysql
哈喽,树先生15 分钟前
1.Seata 1.5.2 seata-server搭建
spring·springcloud
Hello.Reader34 分钟前
StarRocks实时分析数据库的基础与应用
大数据·数据库
执键行天涯36 分钟前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
工作中的程序员1 小时前
ES 索引或索引模板
大数据·数据库·elasticsearch
严格格1 小时前
三范式,面试重点
数据库·面试·职场和发展
微刻时光1 小时前
Redis集群知识及实战
数据库·redis·笔记·学习·程序人生·缓存
单字叶2 小时前
MySQL数据库
数据库·mysql