事务-1 事务隔离级别和Spring事务传播机制

背景

从本文开始开启一个新的专题——事务,用于整理事务相关的概念,包括事务和分布式事务。其中事务为基础内容,分布式事务及其解决方案为事务专题的重点。之所以开启该专题,一方面原因:一直有意愿把事务和分布式事务这一块系统复习并整理一下,因为公司后续拆微服务以及容器化时避免不了处理分布式事务;另一方面:最近修复的一个问题单引发了我对事务—尤其是分布式事务的重视。

本文介绍事务的基础篇:包括事务的概念和性质、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注解信息,即不会为之生成代理对象。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值