spring事务详解(三)源码详解mysql
spring事务详解(四)测试验证spring
在第一节中咱们知道spring为了支持数据库事务的ACID四大特性,在底层源码中对事务定义了6个属性:事务名称、隔离级别、超时时间、是否只读、传播机制、回滚机制。其中隔离级别和传播机制光看第一节的描述仍是不够的,须要实际测试一下方能放心且记忆深入。数据库
模拟用户去银行转帐,用户A转帐给用户B,mybatis
须要保证用户A扣款,用户B加款同时成功或失败回滚。app
测试环境:dom
mysql8+mac,测试时使用的mysql8(和mysql5.6的设置事务变量的语句不一样,不用太在乎)ide
测试准备:
建立一个数据库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)
通用语句:
1.开启/提交事务:开启:begin/start transaction都行,提交:commit;
2.查询事务级别:select @@transaction_isolation;
3.修改事务级别:set global transaction_isolation='read-uncommitted';
注意:修改完了后要exit退出再从新链接mysql(mysql -uroot)才能生效(这里是模拟MySQL5.6,MySQL8有直接生效的语句)。
如下4种测试都是先设置好事务隔离级别,再作的测试,下面的测试就再也不展现出来了。
测试步骤:
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读取的数据明显就错了,即脏读!
测试步骤:
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数据不一致的状况,即不可重复读!
测试步骤:
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当即能够插入成功。
测试步骤:
1.开启2个会话链接mysql,会话1开始事务A,会话2开始事务B。
2.事务A,查询id=2的记录,事务B更新id=2的记录,update操做被阻塞一直到超时(事务A提交后,事务B update能够当即执行)。
以下图(左边的是会话1-事务A,右边的是会话2-事务B):
结论:Serializable级别下,读也加锁!若是是行锁(查询一行),那么后续对这一行的修改操做会直接阻塞等待第一个事务完毕。若是是表锁(查询整张表),那么后续对这张表的全部修改操做都阻塞等待。可见仅仅一个查询就锁住了相应的查询数据,性能实在是不敢恭维。
环境:
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 }
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 }