zoukankan      html  css  js  c++  java
  • spring事务详解(四)测试验证

    系列目录

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

     1 @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
     2     @Override
     3     public void addUserBalanceAndUser(String name, BigDecimal balance) {
     4         log.info("[addUserBalanceAndUser] begin!!!");
     5         //1.新增用户余额--》最终会插入成功,不受嵌套回滚异常影响
     6         UserBalance userBalance = new UserBalance();
     7         userBalance.setName(name);
     8         userBalance.setBalance(new BigDecimal(1000));
     9         this.addUserBalance(userBalance);
    10         //2.新增用户,这里捕获嵌套事务的异常,不让外部事务获取到,不然外部事务肯定会回滚!
    11         try{
    12             // 嵌套事务@Transactional(propagation= Propagation.NESTED, rollbackFor = Exception.class)--》异常会回滚到保存点
    13             userService.addUser(name);
    14         }catch (Exception e){
    15             // 这里可根据实际情况添加自己的业务!
    16             log.error("嵌套事务【addUser】异常!",e);
    17         }
    18 
    19         log.info("[addUserBalanceAndUser] end!!!");
    20     }
  • 相关阅读:
    elasticsearch为什么比mysql快
    elasticsearch(lucene)索引数据过程
    UidGenerator springboot2集成篇
    StampedLock的理解和使用
    EsClientRHL-elasticsearch java客户端开源工具
    Spring只定义接口自动代理接口实现类
    浅谈浏览器存储(cookie、localStorage、sessionStorage)
    图片无缝拼接
    js中变量的连续赋值
    web页面ios浏览器img图片的坑
  • 原文地址:https://www.cnblogs.com/dennyzhangdd/p/9602670.html
Copyright © 2011-2022 走看看