系列目录

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):

总结:明显不行,因为事务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事务详解(四)测试验证的更多相关文章

  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事务详解(转载+高亮)

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

随机推荐

  1. SQLZOO网页中SQL的答案(SELECT from nobel篇)

    SELECT from nobel篇 1. 更改查詢以顯示1950年諾貝爾獎的獎項資料. 答案: SELECT yr, subject, winner FROM nobel WHERE yr = 19 ...

  2. Problem E: 平面上的点和线——Point类、Line类 (V)

    Description 在数学上,平面直角坐标系上的点用X轴和Y轴上的两个坐标值唯一确定,两点确定一条线段.现在我们封装一个“Point类”和“Line类”来实现平面上的点的操作. 根据“append ...

  3. 使用VBA轻松实现汉字与拼音的转换

    Function pinyin(p As String) As String i = Asc(p) Select Case i Case -20319 To -20318: pinyin = &quo ...

  4. mysql_pconnect 问题

    不同于mysql_connect的短连接,mysql_pconnect持久连接的时候,将先尝试寻找一个在同一个主机上用同样的用户名和密码已经打开的(持久)连接,如果找到,则返回此连接标识而不打开新连接 ...

  5. tuxedo 提供buildserver命令编译服务器进程

    转自:http://blog.sina.com.cn/s/blog_5413cc0f0100nbgc.html 事实上buildserver只完成预编译,它会调用当前操作系统中已经安装的默认C编译器来 ...

  6. oracle 表空间不足处理

    linux环境将*.dmp文件导入数据库时,df  -h 发现测试环境虚拟机空间不足,而数据文件xxx.dbf所占空间达到40G左右,果断删除表空间重建在导入. 一.查出存储数据表空间名称 selec ...

  7. 【原创】ACR傻瓜式破解IC芯片卡

    1.简介: 智能卡(英语:Smart card 或IC Card),又称智慧卡.聪明卡.集成电路卡及IC卡,是指粘贴或嵌有集成电路芯片的一种便携式卡片塑料.卡片包含了微处理器.I/O接口及存储器,提供 ...

  8. iOS 二维码 学习

    这段时间忙着交接工作,找工作,找房子,入职,杂七杂八的,差不多一个月没有静下来学习了.这周末晚上等外卖的时间学习一下二维码的制作与扫描. 项目采用OC语言,只要使用iOS自带的CoreImage框架, ...

  9. Kylin引入Spark引擎

    1 引入Spark引擎 Kylin v2开始引入了Spark引擎,可以在构建Cube步骤中替换MapReduce. 关于配置spark引擎的文档,下面给出官方链接以便查阅:http://kylin.a ...

  10. [转]如何正确学习JavaScript

    原文:How to Learn JavaScript Properly(2014-2-7) 学习时长:6-8周 学习前提:中学水平,无需编程经验 更新(2014-1-7) 在Reddit上创建了一个学 ...