spring事务详解(四)测试验证

时间:2022-09-10 19:59:52

系列目录

spring事务详解(一)初探事务

spring事务详解(二)简单样例

spring事务详解(三)源码详解

spring事务详解(四)测试验证

spring事务详解(五)总结提高

一、引子

在第一节中我们知道spring为了支持数据库事务的ACID四大特性,在底层源码中对事务定义了6个属性:事务名称隔离级别超时时间是否只读传播机制回滚机制。其中隔离级别和传播机制光看第一节的描述还是不够的,需要实际测试一下方能放心且记忆深刻。

二、环境

2.1 业务模拟

模拟用户去银行转账,用户A转账给用户B,

需要保证用户A扣款,用户B加款同时成功或失败回滚。

2.2 环境准备

测试环境

mysql8+mac,测试时使用的mysql8(和mysql5.6的设置事务变量的语句不同,不用太在意)

测试准备

创建一个数据库test,创建一张表user_balance用户余额表。id主键,name姓名,balance账户余额。

 1 mysql> create database test;
2 Query OK, 1 row affected (0.05 sec)
3
4 mysql> use test;
5 Database changed
6 mysql> CREATE TABLE `user_balance` (
7 -> `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID主键',
8 -> `name` varchar(20) DEFAULT NULL COMMENT '姓名',
9 -> `balance` decimal(10,0) DEFAULT NULL COMMENT '账户余额',
10 -> PRIMARY KEY (`id`)
11 -> ) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8;
12 Query OK, 0 rows affected, 1 warning (0.15 sec)

初始化数据,2个账户都是1000元:

mysql> INSERT INTO `user_balance` VALUES ('1', '张三', '1000'), ('2', '李四', '1000');
Query OK, 2 rows affected (0.06 sec)
Records: 2 Duplicates: 0 Warnings: 0 mysql> select * from user_balance;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 1000 |
+----+--------+---------+
2 rows in set (0.00 sec)

三、隔离级别实测

3.2 隔离级别实测

通用语句

1.开启/提交事务:开启:begin/start transaction都行,提交:commit;

2.查询事务级别:select @@transaction_isolation;

3.修改事务级别:set global transaction_isolation='read-uncommitted';

注意:修改完了后要exit退出再重新连接mysql(mysql -uroot)才能生效(这里是模拟MySQL5.6,MySQL8有直接生效的语句)。

以下4种测试都是先设置好事务隔离级别,再做的测试,下面的测试就不再展示出来了。

3.2.1 Read Uncommitted(读未提交)

测试步骤:

1.开启2个会话连接mysql,会话1开始事务A,会话2开始事务B。

2.事务A中执行update把张三的余额1000-100=900,事务A查询结果为900。

3.此时事务A并没有提交,事务B查询结果也是900,即:读取了未提交的内容(MVCC快照读的最新版本号数据)。

如下图(左边的是会话1-事务A,右边的是会话2-事务B):

spring事务详解(四)测试验证

总结:明显不行,因为事务A内部的处理数据不一定是最后的数据,很可能事务A后续再加上1000,那么事务B读取的数据明显就错了,即脏读!

3.2.2 Read Committed(读提交)

测试步骤:

1.开启2个会话连接mysql,会话1开始事务A,会话2开始事务B。

2.事务A中执行update把张三的余额1000-100=900,事务A查询结果为900。只要事务A未提交,事务B查询数据都没有变化还是1000.

3.事务A提交,事务B查询立即变成900了,即:读已提交。

如下图(左边的是会话1-事务A,右边的是会话2-事务B)

spring事务详解(四)测试验证

总结:解决了脏读问题,但此时事务B还没提交,即出现了在一个事务中多次查询同一sql数据不一致的情况,即不可重复读!

3.2.3 Repeatable Read(可重读)

测试步骤:

1.开启2个会话连接mysql,会话1开始事务A,会话2开始事务B。

2.事务A中执行update把张三的余额1000-100=900,事务A查询结果为900。事务A提交,事务B查询数据还是1000不变.

3.会话1再开始一个事务C插入一条“王五”数据,并提交,事务B查询还是2条数据,且数据和第一次查询一致,即:读已提交+可重复读。

4.会话2中的事务B也插入一条相同ID的数据,报错:已经存在相同ID=3的数据插入失败!,即出现了幻读。

如下图:

spring事务详解(四)测试验证

mysql支持的解决方案

要防止幻读,可以事务A中for update加上范围,最终会生成间隙锁,阻塞其它事务插入数据,并且当事务A提交后,事务B立即可以插入成功。

spring事务详解(四)测试验证

3.2.4 Serializable(可串行化)

测试步骤:

1.开启2个会话连接mysql,会话1开始事务A,会话2开始事务B。

2.事务A,查询id=2的记录,事务B更新id=2的记录,update操作被阻塞一直到超时(事务A提交后,事务B update可以立即执行)。

如下图左边的是会话1-事务A,右边的是会话2-事务B)

spring事务详解(四)测试验证

结论:Serializable级别下,读也加锁!如果是行锁(查询一行),那么后续对这一行的修改操作会直接阻塞等待第一个事务完毕。如果是表锁(查询整张表),那么后续对这张表的所有修改操作都阻塞等待。可见仅仅一个查询就锁住了相应的查询数据,性能实在是不敢恭维。

四、传播机制实测

3.3.1 测试准备

环境:

spring4+mybatis+mysql+slf4j+logback,注意:日志logback要配置:日志打印为debug级别,这样才能看见事务过程。如下:

1 <root level="DEBUG">
2 <appender-ref ref="STDOUT"/>
3 </root>

测试代码:

测试基类:BaseTest
 1 import lombok.extern.slf4j.Slf4j;
2 import org.junit.runner.RunWith;
3 import org.springframework.boot.test.context.SpringBootTest;
4 import org.springframework.test.context.junit4.SpringRunner;
5 import study.StudyDemoApplication;
6
7 @Slf4j
8 @RunWith(SpringRunner.class)
9 @SpringBootTest(classes = StudyDemoApplication.class)
10 public class BaseTest {
11
12
13 }

测试子类:UserBalanceTest

 1 import org.junit.Test;
2 import study.service.UserBalanceService;
3
4 import javax.annotation.Resource;
5 import java.math.BigDecimal;
6
7 /**
8 * @Description 用户余额测试类(事务)
9 * @author denny
10 * @date 2018/9/4 上午11:38
11 */
12 public class UserBalanceTest extends BaseTest{
13
14 @Resource
15 private UserBalanceService userBalanceService;
16
17 @Test
18 public void testAddUserBalanceAndUser(){
19 userBalanceService.addUserBalanceAndUser("赵六",new BigDecimal(1000));
20 }
21
22 public static void main(String[] args) {
23
24 }
25
26 }
UserBalanceImpl:
 1 package study.service.impl;
2
3 import lombok.extern.slf4j.Slf4j;
4 import org.springframework.stereotype.Service;
5 import org.springframework.transaction.annotation.Propagation;
6 import org.springframework.transaction.annotation.Transactional;
7 import study.domain.UserBalance;
8 import study.repository.UserBalanceRepository;
9 import study.service.UserBalanceService;
10 import study.service.UserService;
11
12 import javax.annotation.Resource;
13 import java.math.BigDecimal;
14
15 /**
16 * @Description
17 * @author denny
18 * @date 2018/8/31 下午6:30
19 */
20 @Slf4j
21 @Service
22 public class UserBalanceImpl implements UserBalanceService {
23
24 @Resource
25 private UserService userService;
26 @Resource
27 private UserBalanceRepository userBalanceRepository;
28
29 /**
30 * 创建用户
31 *
32 * @param userBalance
33 * @return
34 */
35 @Override
36 public void addUserBalance(UserBalance userBalance) {
37 this.userBalanceRepository.insert(userBalance);
38 }
39
40 /**
41 * 创建用户并创建账户余额
42 *
43 * @param name
44 * @param balance
45 * @return
46 */
47 @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
48 @Override
49 public void addUserBalanceAndUser(String name, BigDecimal balance) {
50 log.info("[addUserBalanceAndUser] begin!!!");
51 //1.新增用户
52 userService.addUser(name);
53 //2.新增用户余额
54 UserBalance userBalance = new UserBalance();
55 userBalance.setName(name);
56 userBalance.setBalance(new BigDecimal(1000));
57 this.addUserBalance(userBalance);
58 log.info("[addUserBalanceAndUser] end!!!");
59 }
60 }
如上图所示:

addUserBalanceAndUser(){

  addUser(name);//添加用户

  addUserBalance(userBalance);//添加用户余额
}

addUserBalanceAndUser开启一个事务,内部方法addUser也申明事务,如下:

UserServiceImpl:

 1 package study.service.impl;
2
3 import lombok.extern.slf4j.Slf4j;
4 import org.springframework.stereotype.Service;
5 import org.springframework.transaction.annotation.Propagation;
6 import org.springframework.transaction.annotation.Transactional;
7 import study.domain.User;
8 import study.repository.UserRepository;
9 import study.service.UserService;
10
11 import javax.annotation.Resource;
12
13 /**
14 * @Description
15 * @author denny
16 * @date 2018/8/27 下午5:31
17 */
18 @Slf4j
19 @Service
20 public class UserServiceImpl implements UserService{
21 @Resource
22 private UserRepository userRepository;
23
24 @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
25 @Override
26 public void addUser(String name) {
27 log.info("[addUser] begin!!!");
28 User user = new User();
29 user.setName(name);
30 userRepository.insert(user);
31 log.info("[addUser] end!!!");
32 }
33 }

3.3.2 实测

1.REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

外部方法,内部方法都是REQUIRED:

spring事务详解(四)测试验证

如上图所示:外部方法开启事务,由于不存在事务,Registering注册一个新事务;内部方法Fetched获取已经存在的事务并使用,符合预期。

2.SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。

外部方法required,内部SUPPORTS。

spring事务详解(四)测试验证

如上图,外部方法创建一个事务,传播机制是required,内部方法Participating in existing transaction即加入已存在的外部事务,并最终一起提交事务,符合预期。

3.MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常

外部没有事务,内部MANDATORY:

spring事务详解(四)测试验证

如上图,外部没有事务,内部MANDATORY,报错,符合预期。

4.REQUIRES_NEW:创建新事务,如果存在当前事务,则挂起当前事务。新事务执行完毕后,再继续执行老事务。

外部方法REQUIRED,内部方法REQUIRES_NEW:

spring事务详解(四)测试验证

如上图,外部方法REQUIRED创建新事务,内部方法REQUIRES_NEW挂起老事务,创建新事务,新事务完毕后,唤醒老事务继续执行。符合预期。

5.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

外部方法REQUIRED,内部方法NOT_SUPPORTED

spring事务详解(四)测试验证

如上图,外部方法创建事务A,内部方法不支持事务,挂起事务A,内部方法执行完毕,唤醒事务A继续执行。符合预期。

6.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

外部方法REQUIRED,内部方法NEVER:

spring事务详解(四)测试验证

如上图,外部方法REQUIRED创建事务,内部方法NEVER如果当前存在事务报错,符合预期。

7.NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作。

外部方法REQUIRED,内部方法NEVER:

spring事务详解(四)测试验证

如上图,外部方法REQUIRED创建事务,内部方法NESTED构造一个内嵌事务并创建保存点,内部事务运行完毕释放保存点,继续执行外部事务。最终和外部事务一起commit.上图只有一个sqlSession对象,commit时也是一个。符合预期。

注意:NESTED和REQUIRES_NEW区别?

1.回滚:NESTED在创建内层事务之前创建一个保存点,内层事务回滚只回滚到保存点,不会影响外层事务(真的可以自动实现吗?❎具体见下面“强烈注意”!)。外层事务回滚则会连着内层事务一起回滚;REQUIRES_NEW构造一个新事务,和外层事务是两个独立的事务,互不影响。

2.提交:NESTED是嵌套事务,是外层事务的子事务。外层事务commit则内部事务一起提交,只有一次commit;REQUIRES_NEW是新事务,完全独立的事务,独立进行2次commit。

强烈注意:

NESTED嵌套事务能够自己回滚到保存点,但是嵌套事务方法中的上抛的异常,外部方法也能捕获,那么外部事务也就回滚了,所以如果期望实现内部嵌套异常回滚不影响外部事务,那么需要捕获嵌套事务的异常。如下:

 @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void addUserBalanceAndUser(String name, BigDecimal balance) {
log.info("[addUserBalanceAndUser] begin!!!");
//1.新增用户余额--》最终会插入成功,不受嵌套回滚异常影响
UserBalance userBalance = new UserBalance();
userBalance.setName(name);
userBalance.setBalance(new BigDecimal(1000));
this.addUserBalance(userBalance);
//2.新增用户,这里捕获嵌套事务的异常,不让外部事务获取到,不然外部事务肯定会回滚!
try{
// 嵌套事务@Transactional(propagation= Propagation.NESTED, rollbackFor = Exception.class)--》异常会回滚到保存点
userService.addUser(name);
}catch (Exception e){
// 这里可根据实际情况添加自己的业务!
log.error("嵌套事务【addUser】异常!",e);
} log.info("[addUserBalanceAndUser] end!!!");
}

spring事务详解(四)测试验证的更多相关文章

  1. spring事务详解(五)总结提高

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.概念 ...

  2. spring事务详解(二)简单样例

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

  3. spring事务详解(三)源码详解

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

  4. spring事务详解(一)初探事务

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 引子 很多 ...

  5. 【转】Spring事务详解

    1.事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制,就是利用java.sql.Connection对象完成对事务的提交,那在没有Spring帮我们管理事务 ...

  6. Spring、Spring事务详解;使用XML配置事务

    @Transactional可以设置以下参数: @Transactional(readOnly=false) // 指定事务是否只读的 true/false @Transactional(rollba ...

  7. spring事务详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt122 Spring事务机制主要包括声明式事务和编程式事务,此处侧重讲解声明式 ...

  8. 死磕Spring之AOP篇 - Spring 事务详解

    该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读. Spring 版本:5.1 ...

  9. spring事务详解(转载&plus;高亮)

    spring提供的事务管理可以分为两类:编程式的和声明式的.编程式的,比较灵活,但是代码量大,存在重复的代码比较多:声明式的比编程式的更灵活.编程式主要使用transactionTemplate.省略 ...

随机推荐

  1. 关于vue&period;js中列表渲染练习

    html: <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8 ...

  2. 看见了就转来了, 涉及到UBOOT 地址的一个问题&period;

    addr = (_bss_end + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);什么意思? 这是UBOOT 中的一个分配视频帧缓冲区地址的函数,我想问的是:加一个 ...

  3. 内核移植和文件系统制作(4):UBIFS根文件系统制作总结

    UBIFS文件系统简介: 无排序区块图像文件系统(UnsortedBlock Image File System, UBIFS)是用于固态硬盘存储设备上,并与LogFS相互竞争,作为JFFS2的后继文 ...

  4. POJ 3094 Quicksum&lpar;简单的问题&rpar;

    [简要题意]:题意是非常easy. 看样能理解 [分析]:略. 读取字符串. // 200K 0Ms #include<iostream> using namespace std; int ...

  5. Codeforces Round &num;199 &lpar;Div&period; 2&rpar; B&period; Xenia and Spies

    B. Xenia and Spies time limit per test 2 seconds memory limit per test 256 megabytes input standard ...

  6. SR-IOV简介

    转载:http://docs.oracle.com/cd/E38902_01/html/E38873/glbzi.html SR-IOV 技术是一种基于硬件的虚拟化解决方案,可提高性能和可伸缩性.SR ...

  7. python基本数据类型——set

    一.集合的定义 set集合,是一个无序且不重复的元素集合. 集合对象是一组无序排列的可哈希的值,集合成员可以做字典中的键.集合支持用in和not in操作符检查成员,由len()内建函数得到集合的基数 ...

  8. JavaScript高级程序设计 - 阅读笔记

    [本博客为原创:http://www.cnblogs.com/HeavenBin/] 前言: 大致花费了一个星期的时间把这本书认真看了半本,下面是我做的阅读笔记,希望能够让看这本书的人有个大致的参考. ...

  9. Hadoop伪分布式部署

    一.Hadoop组件依赖关系: 步骤 1)关闭防火墙和禁用SELinux 切换到root用户 关闭防火墙:service iptables stop Linux下开启/关闭防火墙的两种方法 1.永久性 ...

  10. HDU 1017&lpar;&ast;&ast; &ast;&ast;)

    题意是给定 n,m,问有多少组(a,b)满足 0 < a < b < n 而且 (a ^ 2 + b ^ 2 + m) / ( a * b ) 是整数. 直接模拟即可. 代码如下: ...