背景
从本文开始开启一个新的专题——事务,用于整理事务相关的概念,包括事务和分布式事务。其中事务为基础内容,分布式事务及其解决方案为事务专题的重点。之所以开启该专题,一方面原因:一直有意愿把事务和分布式事务这一块系统复习并整理一下,因为公司后续拆微服务以及容器化时避免不了处理分布式事务;另一方面:最近修复的一个问题单引发了我对事务—尤其是分布式事务的重视。
本文介绍事务的基础篇:包括事务的概念和性质、MySql的隔离级别、Spring事务传播机制等,本文偏向于实践操作。
1.事务
事务一般指的是逻辑上一组操作,要么全部执行成功,要么全部执行失败。事务概念可以从事务的特性进行理解,本章节介绍事务的4种特性并结合转账业务进行介绍。
原子性
构成事务的所有操作作为一个整体,要么全部执行成功,要么全部执行失败;不存在部分执行成功,部分失败的场景。
如A给B转账100元行为只可能存在两种结果:执行成功,A账户扣除100元且B账户增加100元;执行失败,A和B账户金额保持不变。
一致性
事务执行前后,从业务侧的角度,系统处于数据一致性状态。A向B转账无论是否成功,银行系统需要保证A和B账户的总额保持不变。
隔离性
并发执行的事务之间相互不可见、互不干扰。A向B转账(事务1)的同时,B向A或者B向C转账(事务2),若事务1没有提交,事务2无法感知。
持久性
事务被提交后会被持久化,不可回滚,从而可以被其他事务感知。A向B转账完成后,表面该事务已执行完成并持久化入数据库,不可回滚,B账户查询余额可以看到转账的结果。
2.MySql数据库事务
数据库一般会并发执行多个事务,而事务的并发执行会带来并发事务问题,如:脏读、不可重复读、幻读等;数据库因此引入了隔离级别的概念。
2.1 隔离级别
数据库的隔离级别按照限制顺序依次包括读未提交、读已提交、可重复读、串行化;隔离级别越高,处理事务并发问题的能力越强,但效率越低。
读未提交
能读取其他事务尚未提交的操作,其他事务回滚时,会导致脏读。
读已提交
只能读取其他事务已提交的操作,不存在脏读;但事务中同一条件多次查询的结果可能不一致,导致不可重复读。读已提交是Oracle和Sql Server的默认隔离级别。
可重复读
在事务中同一条件多次查询的结果一致;但有新纪录插入数据库时,查不到新纪录,但新增、更新、删除等操作影响新记录或受新记录影像,导致幻读。可重复读是Mysql的默认隔离级别。
串行化
串行化作为最高的隔离级别,可以解决幻读的问题;但是所有操作都是串行执行、效率较低,一般业务场景不适用。
2.2 案例介绍
2.2.1 读未提交
环境准备:
【1】将数据库的隔离级别设置为读未提交:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
【2】事务执行前,设置t_account表中用户a的账户为0:
update t_account set money = 0 where name = 'a';
操作步骤:
事务1读取了事物2未提交-被回滚的操作,出现脏读;
2.2.2 读已提交
环境准备:
【1】将数据库的隔离级别设为读已提交:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED
【2】事务执行前,设置t_account表中用户a的账户为0:
update t_account set money = 0 where name = 'a';
操作步骤:
事物1中两次查询用户a账户的金额得到不同结果,出现不可重复读。
2.2.3 可重复度
环境准备:
【1】将数据库的隔离级别设置为可重复读:
【2】事务执行前,设置t_account表中用户a的账户未0:
update t_account set money = 0 where name = 'a';
操作步骤:
发现事物1在两次读取用户a账户得到相同数据,即使过程中事务2已修改了用户a的账户;该隔离级别解决了不可重复读问题。
如前文所述,可重复读隔离级别可导致幻读现象,这里通过两个demo进行介绍:
**demo1:**可以修改查不到的记录,操作步骤如下所示
对事务1而言,在t_account
中查到只有一条记录,但是update操作时受影响的有2条数据,出现幻读现象。
**demo2:**查不到的记录而新增受影响,操作步骤如下所示
说明:t_account
表中用户字段name为唯一索引;
事务1先查询t_account
表—发现没有用户b,然后执行插入用户b的操作—执行失败,失败原因是字段重复,即数据库已存在用户b.
类似:我看到个空杯子,然后往里倒水,水却倒不进去而直接溢出,像是出现了幻觉。
2.2.4 串行化
环境准备:
【1】将数据库的隔离级别设置未读未提交:
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE
【2】事务执行前,设置t_account表中用户a的账户未0:
update t_account set money = 0 where name = 'a';
操作步骤如下所示:
事务1的写操作一直阻塞,直到事务2执行提交或者回滚。
3.Spring事务传播机制
3.1 传播机制
Spring提供了7种事务传播机制以应对不同的业务场景;本章节结合案例分别进行介绍,案例均基于下图模型:
方法A和方法B位于不同的类中,且均为public且非static方法,在A方法中依次调用B1、B2、B3方法。
3.1.1 Required
原则是:有就加入;没有就自创一个
A没有事务时:
B1/B2/B3自行开启事务,此时3个事务相互独立,互补影响;
A存在事务时:
B1/B2/B3加入到事物A中,相互影响;即全部执行成功 或 任何一个方法执行失败—全部回滚;
另外,Required是Spring的默认隔离级别,如在@Transaction注解中不指定隔离级别时—使用Required。
3.1.2 Required_New
原则是:没有新建一个;有,也新建一个相互隔离的事务
A没有事务时:
B1/B2/B3自行开启事务,此时3个事务相互独立,互补影响;
A存在事务时:
B1/B2/B3自行开启事务,共计4个互不影响的事务;任何一个事务失败仅仅回滚自己的事务,不影响其他事务。
即存在事务A和B1/B2执行成功,B3执行失败场景。
3.1.3 Support
原则是:有就加入,没有也行
A没有事务时:
B1/B2/B3以非事务方式运行;
A存在事务时:
B1/B2/B3加入到事务A中,相互影响;即全部执行成功 或 任何一个方法执行失败—全部回滚;
3.1.4 Not_Support
原则是:无论是否有事务,自己以非事务执行
A没有事务时:
B1/B2/B3以非事务方式运行;
A存在事务时:
B1/B2/B3以非事务方式运行;
3.1.5 Mandatory
原则是:必须要有事务,没有抛出异常,有事务-就加入
A没有事务时:
执行B方法时,抛出异常;
A存在事务时:
B1/B2/B3加入到事务A中,相互影响;即全部执行成功 或 任何一个方法执行失败—全部回滚;
3.1.6 never
原则是:不支持事务,有事务就抛出异常
A没有事务时:
B1/B2/B3以非事务方式运行;
A存在事务时:
执行B方法时,抛出异常;
3.1.7 nested
原则是:没有-新建一个事务;有-新建一个子事务(子不影响父,父影响子)
A没有事务时:
B1/B2/B3自行开启事务,此时3个事务相互独立,互补影响;
A存在事务时:
B1/B2/B3自行开启子事务,此时3个事务通过事务A相互关联;
基于嵌套子事务不影响父事务的性质:B1/B2/B3事务执行失败时,只会导致自己对应事务回滚,不影响事务A的正常执行;
基于父事务影响嵌套子事务的性质:A执行失败时,会回滚事务A和所有的事务B.
3.2 案例介绍
以下为基础用例,本章节所有案例均在该基础用例上定制修改,所有案例测试完成后,均回滚数据库至基础用例状态。
数据库
// 创建操作日志表
CREATE TABLE `t_operation_log` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`uname` VARCHAR(100) NOT NULL,
`oper_type` VARCHAR(100) NOT NULL,
`oper_time` VARCHAR(100) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
)
// 创建账户表
CREATE TABLE `t_account` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`Money` INT(10) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name` (`name`) USING BTREE
)
// 新增一条记录:用户a对应的账户为0
INSERT INTO t_account(NAME,money) VALUES('a',0);
代码和业务逻辑
updateMoneyAndLog() 接口的功能是修改t_account 表中用户a的账户为100, 并向日志表t_operation_log中插入一行记录;
// JdbcService.java文件
@Service
public class JdbcService {
private AccountDao accountDao;
private OperationbLogDao operationbLogDao;
public JdbcService(AccountDao accountDao, OperationbLogDao operationbLogDao) {
this.accountDao = accountDao;
this.operationbLogDao = operationbLogDao;
}
public void updateMoneyAndLog() {
// 修改用户a的账户,设置为100
accountDao.updateMoney("a");
// 记录操作日志:记录更新操作
operationbLogDao.insertLog("a", "update");
}
}
// AccountDao.java文件
@Repository
public class AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void updateMoney(String uname) {
jdbcTemplate.update("update t_account set money=100 where name = ?", uname);
}
}
// OperationbLogDao.java文件
@Repository
public class OperationbLogDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void insertLog(String uname, String operationType) {
jdbcTemplate.update("insert into t_operation_log(uname,oper_type,oper_time) values(?,?,?)",
uname, operationType, System.currentTimeMillis());
}
}
测试用例:
@SpringBootTest(classes = {Application.class})
@RunWith(SpringRunner.class)
public class TransactionTests {
@Autowired
private JdbcService jdbcService;
@Test
@Transactional(rollbackFor = Exception.class)
public void contextText() {
jdbcService.updateMoneyAndLog();
}
}
3.2.1 外部方法无注解,内部注解Reuquired
基础用例上修改:在外部方法updateMoneyAndLog后添加抛出异常的语句System.out.println(1/0);
;在内部方法中添加事务注解@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
.
// JdbcService.java文件
public void updateMoneyAndLog() {
// 修改用户a的账户,设置为100
accountDao.updateMoney("a");
// 记录操作日志:记录更新操作
operationbLogDao.insertLog("a", "update");
System.out.println(1/0);
}
// AccountDao.java文件
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void updateMoney(String uname) {
jdbcTemplate.update("update t_account set money=100 where name = ?", uname);
}
// OperationbLogDao.java文件
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertLog(String uname, String operationType) {
jdbcTemplate.update("insert into t_operation_log(uname,oper_type,oper_time) values(?,?,?)",
uname, operationType, System.currentTimeMillis());
}
运行测试用例后,查询数据库得到以下结果:
发现t_account表被修改,t_operation_log表中新增记录。
分析:
因为外部方法未开启事务,因此内部updateMoney(用update事务表示)和insertLog(用insert事务表示)方法分别开启自己的事务,由于insert事务和update事务正常执行完成,因此不会回滚。
基础用例上修改:在内部方法中均添加事务注解@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
.
仅在内部方法insertLog()后添加抛出异常的语句System.out.println(1/0);
;
// JdbcService.java文件
public void updateMoneyAndLog() {
// 修改用户a的账户,设置为100
accountDao.updateMoney("a");
// 记录操作日志:记录更新操作
operationbLogDao.insertLog("a", "update");
}
// AccountDao.java文件
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void updateMoney(String uname) {
jdbcTemplate.update("update t_account set money=100 where name = ?", uname);
}
// OperationbLogDao.java文件
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertLog(String uname, String operationType) {
jdbcTemplate.update("insert into t_operation_log(uname,oper_type,oper_time) values(?,?,?)",
uname, operationType, System.currentTimeMillis());
System.out.println(1/0);
}
运行测试用例后,查询数据库得到以下结果:
发现t_account表被修改,t_operation_log表中无新增记录。
分析:
因为外部方法未开启事务,因此内部updateMoney(用update事务表示)和insertLog(用insert事务表示)方法分别开启自己的事务,且insert事务和update事务相互独立。update事务正常执行完成,insert事务遇到异常回滚。
3.2.2 外部方法注解Reuquired,内部无注解
基础用例上修改:在外部方法updateMoneyAndLog()上添加事务注解@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
,并在方法中添加抛出异常的语句System.out.println(1/0);
;
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void updateMoneyAndLog() {
accountDao.updateMoney("a");
operationbLogDao.insertLog("a", "update");
System.out.println(1 / 0);
}
运行测试用例后,查询数据库得到以下结果:
发现t_account表未被修改,t_operation_log表中无新增记录。
分析:
因为外部方法开启事务,因此内部updateMoney方法和insertLog方法自动加入到事务,与外部事务处于同一事务中。外部方法抛出异常后,导致事务回滚,updateMoney和insertLog均进行回滚。
3.2.3 外部方法注解Reuquired,内部注解Reuquired
// JdbcService.java文件
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void updateMoneyAndLog() {
// 修改用户a的账户,设置为100
accountDao.updateMoney("a");
// 记录操作日志:记录更新操作
operationbLogDao.insertLog("a", "update");
System.out.println(1/0);
}
// AccountDao.java文件
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void updateMoney(String uname) {
jdbcTemplate.update("update t_account set money=100 where name = ?", uname);
}
// OperationbLogDao.java文件
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertLog(String uname, String operationType) {
jdbcTemplate.update("insert into t_operation_log(uname,oper_type,oper_time) values(?,?,?)",
uname, operationType, System.currentTimeMillis());
}
运行测试用例后,查询数据库得到以下结果:
发现t_account表未被修改,t_operation_log表中无新增记录。
分析:
外部方法开启事务,因此内部updateMoney方法和insertLog方法因为添加REQUIRED注解自动加入到外部事务中,与外部事务处于同一事务中。外部方法抛出异常后,导致事务回滚,updateMoney和insertLog均进行回滚。
3.2.4 外部方法注解Reuquired,内部注解Reuquired_New
// JdbcService.java
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void updateMoneyAndLog() {
accountDao.updateMoney("a");
operationbLogDao.insertLog("a", "update");
System.out.println(1 / 0);
}
//OperationbLogDao.java
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void insertLog(String uname, String operationType) {
jdbcTemplate.update("insert into t_operation_log(uname,oper_type,oper_time) values(?,?,?)",
uname, operationType, System.currentTimeMillis());
}
运行测试用例后,查询数据库得到以下结果:
发现t_account表被修改,t_operation_log表中新增记录。
分析:
外部方法因注解REQUIRED开启事务,
因此内部updateMoney方法和insertLog方法因为添加REQUIRED_NEW注解分别开启新事务,与外部事务相互独立。外部方法抛出异常后,导致外部事务回滚,但updateMoney事务和insertLog事务不受影响。
3.2.5 外部方法注解Reuquired,内部注解Support
// JdbcService.java
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void updateMoneyAndLog() {
accountDao.updateMoney("a");
operationbLogDao.insertLog("a", "update");
System.out.println(1 / 0);
}
//OperationbLogDao.java
@Transactional(rollbackFor = Exception.class, propagation = Propagation.SUPPORTS)
public void insertLog(String uname, String operationType) {
jdbcTemplate.update("insert into t_operation_log(uname,oper_type,oper_time) values(?,?,?)",
uname, operationType, System.currentTimeMillis());
}
运行测试用例后,查询数据库得到以下结果:
发现t_account表未被修改,t_operation_log表中无新增记录。
分析:
外部方法因REQUIRED注解开启事务,内部updateMoney方法和insertLog方法因添加SUPPORT注解自动加入到外部事务中,与外部事务处于同一事务中。外部方法抛出异常后,导致事务回滚,updateMoney和insertLog均进行回滚。
3.2.6 外部方法注解Reuquired,内部注解Not_Support
// JdbcService.java
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void updateMoneyAndLog() {
accountDao.updateMoney("a");
operationbLogDao.insertLog("a", "update");
System.out.println(1 / 0);
}
//OperationbLogDao.java
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NOT_SUPPORTED)
public void insertLog(String uname, String operationType) {
jdbcTemplate.update("insert into t_operation_log(uname,oper_type,oper_time) values(?,?,?)",
uname, operationType, System.currentTimeMillis());
}
发现t_account表被修改,t_operation_log表中新增记录。
分析:
外部方法因REQUIRED注解开启事务,内部updateMoney方法和insertLog方法因添加NOT_SUPPORTED注解而不支持事务(可以理解为不具备回滚能力)。外部方法抛出异常后,导致事务回滚,updateMoney和insertLog因不支持事务而不受影响。
3.3 注意事项
Spring提供的@Transaction可以注解在类上或者方法上,方法上的注解会覆盖类上的注解。注解在类上等价于在所有的public方法上添加注解。
因Spring关于@Transaction的实现机制原因,以下场景可能导致事务失效:
(1) 数据库不支持事务
因为Spring的事务是基于JDBC的封装,即本质上依赖于数据库的事务能力;因此当操作MyIsam引擎类型的表时,不具备事务能力。
(2) Bean未被IOC管理
Spring关于@Transaction的实现机制是AOP和动态代理;Spring为注解了@Transaction的bean生成一个代理类,来代理这个对象。因此未被IOC管理的类,不会存在对应的动态代理类。
(3) 同一类中方法调用
如(2)中所述,Spring为目标类(注解了@Transaction的类)生成了一个代理类;当程序从IOC容器中取出目标类时,得到的是代理类。因此,客户端代码并非直接调用目标类代码,而是通过代理类调用目标类的方法。
由此,当目标类中方法调用自己的方法时,不会走代理类,因此同一类中方法调用会导致被调用方法的事务属性丢失。
(4) 非public方法
Spring在读取IOC中的bean时,会检查方法的修饰符是否为public,不是 public则不会获取@Transactional注解信息,即不会为之生成代理对象。