目录
4.2 级别@Transactional的isolation属性
一、事务的相关定义
1.1 定义
将一组操作封装成一个执行单元,要么全部成功,要么全都失败。
举个例子:A给B转账,A的账户减去100,B的账户加+100。如果不将这组操作封装成一个事务,第一步成功了,第二步失败了,那么A的100元就消失了。将这两步操作封装成一个操作,就可以解决这个问题。如果第一步成功第二步失败,这两个步骤就会一起失败。
1.2 事务4大特性ACID
原子性(Atomicity,或称不可分割性):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执过一样。
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
隔离性(Isolation,又称独立性):数据库允许多个并发事务同时对数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化
(Serializable)。
二、Spring中事务的实现
分为两类,一种是编程式事务(需要手动写代码操作事务),第二种是声明式事务(利用注解自动开启和提交事务)。
2.1 手动操作事务的三个步骤
获取事务、提交事务、回滚事务
SpringBoot 内置了两个对象,DataSourceTransactionManager 用来获取事务(开启事务)、提交或回滚事务的,而 TransactionDefinition 是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从而获得一个事务 TransactionStatus。
手动实现事务,操作很繁琐。声明式事务操作简单,主要学习声明式事务。
三、Spring声明式事务@Transactional
3.1 Spring声明式事务的简单使用
声明式事务的实现很简单,只需要在需要的方法上加上@Transactional注解就可以实现,不需要手动开启事务和提交事务,在进入方法时自动开启事务,方法执行完会自动提交事务。如果中途发生了没有处理的异常会自动回滚事务。
@Transactional
@RequestMapping("/hello")
public String sayhi() throws InterruptedException {
try(OutputStream outputStream = new FileOutputStream("a.txt")){
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
}catch (IOException e){
e.printStackTrace();
}
int a = 1/0;
return "hello";
}
如果不加@Transactional,程序运行结束后a.txt中的内容
如果加了@Transactional,程序运行结束后a.txt中的内容
发生了回滚,当发生异常后,两个操作一起失败。
3.2 @Transactional作用范围
@Transactional可以用来修饰方法或类
修饰方法时:只能应用到public方法上,否则不生效。
修饰类时:表明该注解会对该类中所有的public方法都生效
3.3 @Transactional参数说明
参数 | 作用 |
propagation | 事务的传播行为。默认值是Propagation.REQUIRED |
isolation | 事务的隔离级别。默认值是Isolation.DAFAULT |
timeout | 事务的超时时间,默认值是-1.如果超过该事件限制但事务还没有完成,则自动回滚事务。 |
readOnly | 指定事务是否为只读事务,默认值是false。设置为true,可以忽略那些不需要事务的方法,比如读取数据 |
rollbackFor | 指定触发事务回滚的异常类型,可以指定多个异常类型 |
rollbackForClassName | 指定触发事务回滚的异常类型,可以指定多个异常类型 |
noRollbackFor | 抛出指定的异常类型,不会滚事务,也可指定多个异常类型 |
noRollbackForClassName | 抛出指定的异常类型,不会滚事务,也可指定多个异常类型 |
propagation和isolation在后文会有详细的介绍,在本小结,暂不介绍。
3.3.1 timeout
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Transactional(timeout = 6)
@Test
void addUser() throws InterruptedException {
userMapper.addUser("xiaohei","1234");
Thread.sleep(6060);
}
}
设置事务的超时时间是6s,在程序中加了一个6060ms的sleep,导致事务的运行事件超过6s,事务会发生回滚,增加用户操作没有成功。
3.3.2 readOnly
同时读取数据并不会造成异常,因此,将读取的操作不必设置为一个事务。
3.4 如果异常被捕获,不会进行事务回滚
3.4.1 问题描述
情况1:对异常不做处理,事务会发生回滚
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Transactional
@Test
void addUser() throws InterruptedException {
userMapper.addUser("xiaohei","1234");
int a = 1/0;
}
}
情况2:@Transactional 在异常被捕获的情况下,不会进行事务自动回滚
站在Spring的角度,如果异常发生了,并把异常处理了,Spring就认为没有发生异常,就不会回滚,就会导致程序发生异常但是数据被写入了数据库
@Transactional
@RequestMapping("/login")
public void addUser(){
String username = "xiaobai";
String password = "1111";
System.out.println(1);
userMapper.addUser(username,password);
try{
int a = 1/0;
}catch (ArithmeticException e){
System.out.println(e.getMessage());
}
}
3.4.2 解决方案1:将异常重新抛出
@Transactional
@RequestMapping("/login")
public void addUser(){
String username = "xiaohei";
String password = "1122";
userMapper.addUser(username,password);
try{
int a = 1/0;
}catch (ArithmeticException e){
System.out.println(e.getMessage());
throw e;
}
}
3.4.3解决方案2:手动回滚事务
手动回滚事务,在方法中使用 TransactionAspectSupport.currentTransactionStatus() 可
以得到当前的事务,然后设置回滚方法 setRollbackOnly 就可以实现回滚。
@Transactional
@RequestMapping("/login")
public void addUser(){
String username = "xiaohei";
String password = "1122";
userMapper.addUser(username,password);
try{
int a = 1/0;
}catch (ArithmeticException e){
System.out.println(e.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
默认情况下,Spring中的事务如果遇到运行时异常,那么事务一定会进行回滚。如果遇到非运行时异常,想要回滚,那么需要设置rollbackFor=非运行时异常类.
四、事务隔离级别
4.1 设置方式
事务有原子性、一致性、持久性和隔离性四种特性。只有隔离性是可以设置的。
设置事务的隔离级别可以保证多个并发事务执行时更可控,即防止不同事务之间相互影响。使得运行结果更加符合操作者的预期。
Spring中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置。
4.2 级别@Transactional的isolation属性
4.2.1 MySQL的事务隔离级别(4种)
READ UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。
READ COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。
REPEATABLE READ:可重复读,是 MySQL 的默认事务隔离级别,它能确保同一事务多次查询的结果一致。但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数
据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败
(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读
(Phantom Read)。
SERIALIZABLE:序列化,事务最高隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交READ UNCOMMITTED | √ | √ | √ |
读已提交READ COMMITTED | × | √ | √ |
可重复读REPEATABLE READ | × | × | √ |
串行化SERIALIZABLE | × | × | × |
脏读:一个事务读取到了另一个事务修改的数据之后,后一个事务又进行了回滚操作,从而导致
第一个事务读取的数据是错误的。
不可重复读:一个事务两次查询得到的结果不同,因为在两次查询中间,有另一个事务把数据修
改了。(修改)
幻读:一个事务两次查询中得到的结果集不同,因为在两次查询中另一个事务有新增了一部分数
据。(增加删除)
4.2.2 Spring 中事务隔离级别(5种)
Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。
Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级
别)。
Isolation.SERIALIZABLE:串行化,可以解决所有并发问题,但性能太低。
相比于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了一个Isolation.DEFAULT(以数据库的全局事务隔离级别为主)。
五、Spring事务传播机制
5.1 定义
定义了多个包含了事务的方法,相互调用时,事务是如何在这些方法间进行传递的。
事务隔离级别是保证多个并发事务执行的可控性的(稳定性的),而事务传播机制是保证一个事务在多个调用方法间的可控性的(稳定性的)。
Spring事务传播机制可以在@Transactional的propagation属性中设置。
5.2 分类
Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一⼀个新的事务。
Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。
Propagation.MANDATORY:mandatory:强制性)如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。
Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。
Propagation.NOT_SUPPORTED:以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂
Propagation.NEVER:以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常
Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。
5.3 Spring事务传播机制使用和演示
5.3.0 准备工作
验证思路:有两个事务,第一个事务是新增博客,第二个事务是新增用户,给事务二的执行加一个异常,在事务一执行的时候同时执行事务二,观察设置不同类型的事务传播机制下,异常发生时,两个事务是否会发生回滚。
step1:建立BlogMapper接口和BlogMapper.xml文件,实现一个新增博客方法。
package com.example.demo.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BlogMapper {
public void addBlog(String blogTitle,String blogContent);
}
<?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">
<!--namespace=要实现的接口的完整包名和类名-->
<mapper namespace="com.example.demo.mapper.BlogMapper">
<insert id="addBlog">
insert into blog(blogTitle,blogContent) values(#{blogTitle},#{blogContent})
</insert>
</mapper>
step2:建立UserMapper接口和UserMapper.xml文件,实现一个新增用户方法。
package com.example.demo.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
public int addUser(String userName,String password);
}
<?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">
<!--namespace=要实现的接口的完整包名和类名-->
<mapper namespace="com.example.demo.mapper.UserMapper">
<insert id="addUser">
insert into user(userName,password) values(#{userName},#{password})
</insert>
</mapper>
step3:建立一个BlogService,调用BlogMapper的addBlog()方法和User Service里的addUser()方法
package com.example.demo.service;
import com.example.demo.mapper.BlogMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class BlogService {
@Resource
private BlogMapper blogMapper;
@Autowired
private UserService userService;
@Transactional(propagation = Propagation.NESTED)
public void addBlog(){
String str1 = "asdfgh123111";
String str2 = "asdfgh1234111";
blogMapper.addBlog(str1,str2);
userService.addUser();
}
}
step4:建立一个UserService,调用UserMapper的addUser()方法,同时,在UserService里面加一个异常。
package com.example.demo.service;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(propagation = Propagation.NESTED)
public void addUser(){
String username = "xiaohei11111";
String password = "11211112";
userMapper.addUser(username,password);
try{
int a = 1/0;
}catch(Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
step5:建立Controller类,调用BlogService
package com.example.demo;
import com.example.demo.service.BlogService;
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
public class Controller {
@Autowired
private BlogService blogService;
@RequestMapping("/func1")
@Transactional
public void func1(){
blogService.addBlog();
}
}
5.3.1 REQUIRED
5.3.2 REQUIRES_NEW
5.3.3 NESTED嵌套事务
5.3.4 REQUIRED和NESTED嵌套事务的区别
嵌套事务:回滚有问题的事务,但主事务不受影响。主事务没回滚。
加入事务:如果任意一个方法出现异常,那么整个事务(包括主事务)都会回滚。
六、总结
事务是指将一组操作封装成一个执行单元,要么全部成功,要么全都失败。Spring中事务的实现分为编程式事务和声明式事务两种。后者使用最多,在方法上加上@Transactional注解即可。Spring中事务的隔离级别有五种,在@Transactional注解的isolation 属性可以进行设置。Spring事务的传播机制分为六种,可以在@Transactional的propagation属性进行设置。当事务传播机制是嵌套事务时,回滚有问题的事务,但主事务不受影响,主事务没回滚。当事务传播机制是加入事务时,如果任意一个方法出现异常,那么整个事务(包括主事务)都会回滚。