一:什么是事务?
事务定义:将⼀组操作封装成⼀个执⾏单元(封装到⼀起),要么全部成功,要么全部失败。
二:Spring中事务的实现
- 编程式事务(⼿动写代码操作事务)。
- 声明式事务(利⽤注解⾃动开启和提交事务)。
实际项目开发中,我们更倾向于使用“声明式事务”,因为它既方便又高效。
三:编程式事务
3.1 步骤
- 开启事务;
- 提交事务;
- 回滚事务。
3.2 示例
application.yml
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1/myblog?characterEncoding=utf8
username: root
password: 111111
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 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
UserMapper.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.UserMapper">
<insert id="add">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null">
username,
</if>
<if test="photo != null">
photo,
</if>
<if test="password != null">
password,
</if>
</trim>
<trim prefix="values(" suffix=")" suffixOverrides=",">
<if test="username != null">
#{username},
</if>
<if test="photo != null">
#{photo},
</if>
<if test="password != null">
#{password},
</if>
</trim>
</insert>
</mapper>
UserController [重点]
package com.example.demo.controller;
import com.example.demo.service.UserService;
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.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/add")
public int add(String username,String password) {
//非空校验
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return 0;
}
//事务[得到并开启事务]
TransactionStatus transactionStatus =
dataSourceTransactionManager.getTransaction(transactionDefinition);
int result = userService.add(username,password,null);
System.out.println("受影响的行数:" + result);
//提交事务 or 回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
//dataSourceTransactionManager.commit(transactionStatus);
return result;
}
}
UserService
package com.example.demo.service;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public int add(String username,String password,String photo) {
return userMapper.add(username,password,photo);
}
// public List<UserInfo> getAll() {
//
// }
}
启动项目,先使用提交事务:
查看数据库中的数据:
查看结果:
说明事务提交成功。
使用回滚事务:
再次查看数据库中的数据,并没有添加成功,说明事务实现了回滚。
解释:
四:声明式事务
4.1 示例
同时别忘记在UserMapper.xml中进行配置:
相比上面的编程式事务,声明式事务的写法就会优雅许多,我们来添加一条数据:
到底成功否?查看数据库揭晓答案:
现在我们手动添加一条异常并再次添加数据,如果事务实现了回滚,那么数据库中应该不会出现这条数据:
这很容易想到,都直接500错误了,必不可能添加成功了。
那如果我们将这个异常try/catch起来呢?
此时返回受影响的行数为1,且数据添加成功。
说明官方在设计出现异常回滚事务时,要看出现的异常有没有被捕获。如果异常被捕获,就认为你已经处理了这个异常,所以会被像正常情况一样添加成功。
那如果在这种情况下,我希望继续回滚事务呢?我介绍两种操作方法:
4.2 @Transactional的工作原理
@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。@Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。
@Transactional 实现思路:
@Transactional 具体执行细节:
三:事务隔离级别
3.1 事务特性
事务有4 ⼤特性(ACID),原⼦性、持久性、⼀致性和隔离性,具体概念如下:
- Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
设置事务的隔离级别,是为了保障多个并发事务执行更可控,更符合操作者预期,是为了防止其余事务影响当前事务的一种策略。
3.2 MySQL中的隔离级别
Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(比如Oracle,但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理期间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行,就好像出现幻觉一样。
Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
-
脏读:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错误的。
-
不可重复读:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。
-
幻读:⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务又新增了⼀部分数据。
总结:
- 脏读是可能读取到未被提交的数据的,而不可重复读和幻读读取到的一定是已提交的数据。
- 不可重复读侧重于两次查询中间数据被修改,而幻读侧重于两次查询中间数据的添加或删除。
3.3 Spring中的隔离级别
⽽ Spring 中事务隔离级别包含以下 5 种:
- Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
- Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
- Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。
- Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。
- Isolation.SERIALIZABLE:串行化,可以解决所有并发问题,但性能太低。
从上述介绍可以看出,相⽐于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个Isolation.DEFAULT(以数据库的全局事务隔离级别为主)。
Spring中事务隔离级别可以通过@Transactional中的isolation属性进行设置。
四:Spring事务传播机制
4.1 什么是事务传播机制?
Spring事务传播机制定义了多个包含了事务的方法,相互调用时,事务是如何在这些方法间进行传递的。
4.2 为什么需要事务传播机制?
事务隔离级别是保证多个并发事务执⾏的可控性的(稳定性的),⽽事务传播机制是保证⼀个事务在多个调⽤⽅法间的可控性的(稳定性的)。
4.3 事务传播机制有哪些?
Spring 事务传播机制包含以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以非事务的⽅式继续运⾏。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独⽴,互不干扰。
- Propagation.NOT_SUPPORTED:以非事务方式运⾏,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以非事务⽅式运⾏,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
以上 7 种传播⾏为,可以根据是否⽀持当前事务分为以下 3 类:
以情侣关系为例来理解以上分类:
4.4 实例
下面就演示一下默认的事务传播级别和嵌套事务传播级别,也是面试中最常考到的两个事务传播级别:
4.4.1 默认事务传播级别
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.REQUIRED)
public int add(String username,String password,String photo) {
return userMapper.add(username,password,photo);
}
//添加用户信息
@Transactional(propagation = Propagation.REQUIRED)
public int save(String username,String password,String photo) {
try {
int num = 10 / 0;
} catch (Exception e) {
System.out.println("异常:" + e.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return userMapper.add(username,password,photo);
}
}
UserController
package com.example.demo.controller;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/add2")
@Transactional(propagation = Propagation.REQUIRED)
public int add2(String username, String password) {
// 非空效验
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return 0;
}
int result = userService.add(username, password, null);
System.out.println("添加影响行数:" + result);
int result2 = userService.save(username, password, null);
System.out.println("添加影响行数:" + result2);
return result;
}
}
这里的add和save方法都是添加用户,可能业务比较奇葩,但是能说明问题就行。此时两个事务的传播级别都是默认事务传播级别,我在save方法中整个幺蛾子,根据“一荣俱荣,一损俱损”的原则,那么两条数据应该都不会添加成功。
查看数据库中数据:
没有发生变化。
4.4.2 嵌套事务传播级别
修改save方法为嵌套事务传播级别。
查看数据库中数据,发现有一条数据添加成功了,这是因为第二次事务虽然失败,但由于其是默认事务传播级别,所以不影响第一次事务的执行。
总结:
嵌套事务(NESTED)和加⼊事务(REQUIRED )的区别:
整个事务如果全部执⾏成功,⼆者的结果是⼀样的。
如果事务执⾏到⼀半失败了,那么加⼊事务整个事务会全部回滚;而嵌套事务会局部回滚,不会影响上⼀个⽅法中执行的结果。
五:总结
本课内容到此结束!