系列目录
一、引子
在第一节中我们知道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):
总结:明显不行,因为事务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):
总结:解决了脏读问题,但此时事务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的数据插入失败!,即出现了幻读。
如下图:
mysql支持的解决方案:
要防止幻读,可以事务A中for update加上范围,最终会生成间隙锁,阻塞其它事务插入数据,并且当事务A提交后,事务B立即可以插入成功。
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):
结论: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:
如上图所示:外部方法开启事务,由于不存在事务,Registering注册一个新事务;内部方法Fetched获取已经存在的事务并使用,符合预期。
2.SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
外部方法required,内部SUPPORTS。
如上图,外部方法创建一个事务,传播机制是required,内部方法Participating in existing transaction即加入已存在的外部事务,并最终一起提交事务,符合预期。
3.MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常
外部没有事务,内部MANDATORY:
如上图,外部没有事务,内部MANDATORY,报错,符合预期。
4.REQUIRES_NEW:创建新事务,如果存在当前事务,则挂起当前事务。新事务执行完毕后,再继续执行老事务。
外部方法REQUIRED,内部方法REQUIRES_NEW:
如上图,外部方法REQUIRED创建新事务,内部方法REQUIRES_NEW挂起老事务,创建新事务,新事务完毕后,唤醒老事务继续执行。符合预期。
5.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
外部方法REQUIRED,内部方法NOT_SUPPORTED:
如上图,外部方法创建事务A,内部方法不支持事务,挂起事务A,内部方法执行完毕,唤醒事务A继续执行。符合预期。
6.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
外部方法REQUIRED,内部方法NEVER:
如上图,外部方法REQUIRED创建事务,内部方法NEVER如果当前存在事务报错,符合预期。
7.NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作。
外部方法REQUIRED,内部方法NEVER:
如上图,外部方法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事务详解(四)测试验证的更多相关文章
-
spring事务详解(五)总结提高
系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.概念 ...
-
spring事务详解(二)简单样例
系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...
-
spring事务详解(三)源码详解
系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...
-
spring事务详解(一)初探事务
系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 引子 很多 ...
-
【转】Spring事务详解
1.事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制,就是利用java.sql.Connection对象完成对事务的提交,那在没有Spring帮我们管理事务 ...
-
Spring、Spring事务详解;使用XML配置事务
@Transactional可以设置以下参数: @Transactional(readOnly=false) // 指定事务是否只读的 true/false @Transactional(rollba ...
-
spring事务详解
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt122 Spring事务机制主要包括声明式事务和编程式事务,此处侧重讲解声明式 ...
-
死磕Spring之AOP篇 - Spring 事务详解
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读. Spring 版本:5.1 ...
-
spring事务详解(转载+高亮)
spring提供的事务管理可以分为两类:编程式的和声明式的.编程式的,比较灵活,但是代码量大,存在重复的代码比较多:声明式的比编程式的更灵活.编程式主要使用transactionTemplate.省略 ...
随机推荐
-
关于vue.js中列表渲染练习
html: <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8 ...
-
看见了就转来了, 涉及到UBOOT 地址的一个问题.
addr = (_bss_end + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);什么意思? 这是UBOOT 中的一个分配视频帧缓冲区地址的函数,我想问的是:加一个 ...
-
内核移植和文件系统制作(4):UBIFS根文件系统制作总结
UBIFS文件系统简介: 无排序区块图像文件系统(UnsortedBlock Image File System, UBIFS)是用于固态硬盘存储设备上,并与LogFS相互竞争,作为JFFS2的后继文 ...
-
POJ 3094 Quicksum(简单的问题)
[简要题意]:题意是非常easy. 看样能理解 [分析]:略. 读取字符串. // 200K 0Ms #include<iostream> using namespace std; int ...
-
Codeforces Round #199 (Div. 2) B. Xenia and Spies
B. Xenia and Spies time limit per test 2 seconds memory limit per test 256 megabytes input standard ...
-
SR-IOV简介
转载:http://docs.oracle.com/cd/E38902_01/html/E38873/glbzi.html SR-IOV 技术是一种基于硬件的虚拟化解决方案,可提高性能和可伸缩性.SR ...
-
python基本数据类型——set
一.集合的定义 set集合,是一个无序且不重复的元素集合. 集合对象是一组无序排列的可哈希的值,集合成员可以做字典中的键.集合支持用in和not in操作符检查成员,由len()内建函数得到集合的基数 ...
-
JavaScript高级程序设计 - 阅读笔记
[本博客为原创:http://www.cnblogs.com/HeavenBin/] 前言: 大致花费了一个星期的时间把这本书认真看了半本,下面是我做的阅读笔记,希望能够让看这本书的人有个大致的参考. ...
-
Hadoop伪分布式部署
一.Hadoop组件依赖关系: 步骤 1)关闭防火墙和禁用SELinux 切换到root用户 关闭防火墙:service iptables stop Linux下开启/关闭防火墙的两种方法 1.永久性 ...
-
HDU 1017(** **)
题意是给定 n,m,问有多少组(a,b)满足 0 < a < b < n 而且 (a ^ 2 + b ^ 2 + m) / ( a * b ) 是整数. 直接模拟即可. 代码如下: ...