最近一个月在看kafka那本书,之前看mysql和redis都会对学习过程中的知识做一些记录以及自己的看法,但前提是真的对某一个知识点有一定的了解;这个了解不是说一定要到很精通的程度,只要自己对知识点有一定的见解就够了,就像Innodb的缓存池,操作系统和kafka,如果你真的有深入了解过,那你便会豁然开朗,其当中的设计思想无非就是磁盘加缓存,预读加后写;当然其中还有一些很精妙的数据结构的设计结合,这需要感兴趣的人去深挖,在设想一下我们在高并发下的请求入队以及本地缓存和分布式缓存,何尝不是操作系统的扩大版和简化版呢!
公司内部有技术分享,正好轮到我们组,所以就做了一份PPT用以借鉴,在这里记录下我整理的《spring中事务-原理介绍和应用》,之前有写过mysql中的事务,回头看了一下写的很简洁,所以在这里已编码的方式重新赘述一下。
从事务的隔离级别和spring事务的传播性两个方面入手:
事务的隔离级别
其实单纯的说起事务的隔离级别我们都已经耳熟能祥了,但是我敢说如果没有动手去体验过,相信你在不久的时间间隔内绝对会忘记,甚至有四种还是五种隔离级别你都会记混淆。
在说隔离级别之前,有必要了解下<b>脏写</b>是什么情况;
脏写:一个事务修改了另一个未提交事务修改过的数据。怎么理解呢?先假设user表中id为1的用户年龄为0;
SessionA | SessionB |
---|---|
start transaction; | |
- | start transaction; |
- | update user set age = 2 where id =1 |
update user set age = 1 where id =1 | |
commit; | |
- | rollback; |
如上表展示,当SessionB回滚时将SessionA的修改也会滚了,这样看着A好像啥事也没干,这种情况就称之为脏写;
再说说隔离级别吧!SQL:1992标准中描述了四种事务隔离级别 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE;Innodb均支持这四种隔离级别;
其中我们经常说的锁跟隔离级别是分不开的,在不同的隔离级别下innodb使用不同的锁定策略(这篇文章的重点不在锁,数据库中的锁后面会专门写一个章节),接下来我们一个一个看
下面每个点会通过具体的代码描述涉及到的表就一个user表
create table user(
id bigint auto_increment primary key ,
name varchar(32) default null,
age int default null
);
insert into user (name,age) values ('老王',1),('老张',2);
脏读
顾名思义,就是未提交读,首先简单的从字面意思来理解无非就是一个事务读了另一个事务未提交的记录;其实从数据库的角度看,从字面意思理解也八九不离十了
SessionA | SessionB |
---|---|
start transaction; | |
- | start transaction; |
- | update user set age = 2 where id =1 |
select age from user where id =1 | |
commit; | |
- | rollback; |
如上所示,当SessionA读取到的age为2时就符合脏读的情况了,也就是在该隔离级别下会发生脏读(不可重复读,幻读)
通过代码看下脏读的情况
//该方法属于UserService0_1
public void getUserById(User user) throws InterruptedException {
//使用默认的TransactionDefined传播性是REQUIRED,不符合条件,所以在这里重新定义了一个,REQURIED_NEW传播特性的事务
TransactionDefinition transactionDefinition = new MyTranstractionDefined();
TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
//在一个事务中读取用户信息并打印
User id1 = userDao.getUserById(user.getId());
log.error("---------------"+id1.getName()+"->"+id1.getAge()+"---------");
transactionManager.commit(status);
}
//该方法属于userSerive02
public void testReadUncommited(User user){
TransactionDefinition transactionDefinition = new MyTranstractionDefined();
TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
try {
//在当前事务中更新用户
userDao.updateUserAge(user);
//开启一个事务读取被更新的数据,注意当前还没有提交
userService01.getUserById(user);
//在这里认为制造异常回滚
int i = 1/0;
transactionManager.commit(status);
}catch (Exception e){
transactionManager.rollback(status);
}
}
//测试代码
@Test
public void testREAD_UNCOMMITED(){
User user = new User();
user.setId(1L);
user.setAge(2);
userService02.testReadUncommited(user);
}
操作步骤
1.首先使用以下命令查看和修改数据库的隔离级别(在第一步先不执行第二条sql)
#查看数据库隔离级别
show variables like 'transaction_isolation';
#设置数据库隔离级别(READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE)
# global是全局的 session是当前会话
set global transaction isolation level READ UNCOMMITTED
建议先执行上述第一条命令查看当前隔离级别,如果之前没有设置过隔离级别,应该是如下图所示
2.在REPEATABLE_READ隔离界别下执行测试用例显示如下结果
可以看到老王的年龄没有被更新为2,而且读到了更新前的数据,我们先记下结果,执行第三步
3.执行第一步中的第二条sql语句,将数据库隔离级别设置为READ UNCOMMITTED(执行完后要查看事务隔离级别需要关闭当前客户端重新连接)
然后执行测试用例再看下输出结果
我们可以看到,读取到的数据跟数据库中的不一致,接下来总结一下
总结
在READ_UNCOMMITTED隔离级别下会发生脏读、不可重复读和幻读,(至于不可重复读和幻读我会在稍后的隔离级别下介绍);从MVCC层面来说就是读到版本链中最新的一条
至于什么是MVCC
不可重复读
定义:一个事务读取到了另一个已提交事务修改过的最新数据那么就发生了不可重复读;单从定义上理解有点扰人心魂,接下来从图表和代码两个方面演示
SessionA | SessionB |
---|---|
start transaction; | |
select age from user where id =1(age=1) | - |
- | update user set age = 2 where id =1 |
select age from user where id =1(age=2) | |
commit; | - |
如上表所示,在事务A中查询id = 1的记录时,读到的age为2,也就是sessionA中读到了SessionB更新后的结果;也就是不可重复读,接下来用代码演示一下
//该方法属于UserService0_1
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUser(User user){
userDao.updateUserAge(user);
log.error("更新成功");
}
//该方法属于userSerive02
@Transactional(propagation = Propagation.REQUIRED)
public void testReadCommitted(User user1) throws InterruptedException {
User id = userDao.getUserById(user1.getId());
log.error("first read:"+id.getAge());
userService01.updateUser(user1);
Thread.sleep(5000);
User id1 = userDao.getUserById(user1.getId());
log.error("second read:"+id1.getAge());
}
//测试代码
@Test
public void testREADCOMMITTED() throws InterruptedException {
User user1 = new User();
user1.setId(1L);
user1.setAge(2);
userService02.testReadCommitted(user1);
}
操作步骤
1.将数据库隔离级别设置为 READ UNCOMMITTED|READ COMMITTED
2.执行测试代码得到结果,first read:1;second read:2
总结
在READ_COMMITTED隔离级别下会发生不可重复读和幻读;从MVCC讲就是:READ_COMMITTED隔离级别在每次查询开始时都会生成一个独立的ReadView,
然后creator_trx_id和min_trx_id与max_trx_id做比较再通过roll_pointer回滚;读取小于min_trx_id的一个版本
注意
1.如果在执行上述代码,first read 和second read的结果都一样,检查下mybatis是否禁用掉了一级缓存,如果没有那么请禁用一下
2.代码只演示了不可重复读的情况但是没有演示幻读,但是结论却说有幻读,是不是不太严谨?因为在接下来会演示幻读代码,到时候切换下隔离级别即可,防止啰嗦代码出现
幻读
定义:A事务进行范围查找,此时B事务新增了符合A查找范围的记录,然后被A读取到了这种情况就被称为幻读
SessionA | SessionB |
---|---|
start transaction; | |
select * from user where age >1[{老张:2}] | - |
- | insert into user (name,age) values("老李",3) |
select * from user where age > 1[{老张:2},{老李:3}] | |
commit; | - |
如上表所示,在事务A中查询age > 1的记录时,读到的[{老张:2},{老李:3}],也就是sessionA中读到了SessionB新增后的结果;也就是幻读,接下来用代码演示一下
//该方法属于UserService0_1
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insertUser(User user){
userDao.insertUser(user);
log.error("插入成功");
}
//该方法属于userSerive02
@Transactional(propagation = Propagation.REQUIRED)
public void testREPEATABLEREAD(Integer age,User user) throws InterruptedException {
List<User> users = userDao.listUserByParam(age);
String first = users.stream().map(User::getName).collect(Collectors.joining(","));
log.error("first read:"+first);
userService01.insertUser(user);
Thread.sleep(5000);
List<User> users1 = userDao.listUserByParam(age);
String second = users1.stream().map(User::getName).collect(Collectors.joining(","));
log.error("second read:"+second);
}
//测试方法
@Test
public void testREPEATABLEREAD() throws InterruptedException {
User user = new User();
user.setAge(3);
user.setName("老李");
userService02.testREPEATABLEREAD(1,user);
}
操作步骤
1.将数据库隔离级别设置为 REPEATABLE READ
2.执行测试代码得到结果,first read:{老张:2};second read:{老张:2}
总结
咦,为什么没有出现幻读的情况?
- 有个比较有争议的话题,我们测试REPEATABLE_READ隔离级别的时候看到,并没有发生幻读,有人却说mysql是通过mvcc解决幻读的,这是真的吗?(其实这里还要深究下去)
怎么说呢? 对于SQL92标准来说REPEATABLE_READ是不能解决幻读的,但是mysql却可以,而且mysql又可以通过mvcc解决的,还可以通过next-key-lock解决。
- 针对幻读,还有人说我如果删除了符合条件的数据,结果读出来的数据少了,这个算不算幻读呢?
答:不算,具体可以参考下SQL92标准的定义
3.老是mvcc,到底有什么用?
事务利用MVCC进行的读取操作称之为一致性读,或者一致性无锁读,有的地方也称之为快照读。所有普通的SELECT语句(plain SELECT)在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读;
一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动
数据库的隔离级别就到这里先告一段落,接下来我们看看spring中事务的传播特性
Spring事务的传播特性
spring中事务相关核心API
如下图所示在spring中事务相关的api都是通过实现或者继承TransactionManager,我们重点的看下PlatformTransactionManager这个类
PlatformTransactionManager
在上图中我圈出了三个类分别为PlatformTransactionManager、TransactionStatus和TransactionDefinition
1.<b>TransactionStatus:<b/>直译过来是事务的状态,我们点进去看这个类继承了TransactionExecution、SavepointManager、Flushable三个类;分别定义了事务的一些行为比如回滚,是否是新事务,是否完成;事务的回滚点创建回滚等功能;
2.<b>TransactionDefinition:<b/>事务行为的定义,该类中定义了事务的隔离级别和传播特性,接下来详细解释下每个传播特性
/**
* 测试 transactionTemplate api
*/
public void test1(final User user){
TransactionCallback callback = new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
//1.发生异常不抓捕时会自动回滚
userDao.updateUserAge(user);
int x = 4/0;
}catch (Exception e){
//2.不手动指定回滚的话当前修改不会回滚
//3.不信的话把catch去掉试试,应该会回滚,
transactionStatus.setRollbackOnly();
}
}
};
transactionTemplate.execute(callback);
}
/**
* 测试 transactionManager api
*/
public void test2(User user){
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userDao.updateUserAge(user);
//1.事务不手动提交的话是不会更新成功的,不信你试试,这里涉及到了刷脏
transactionManager.commit(status);
//提交之后的回滚会有用吗? 自己试试?刷脏完了然后回滚有用
int x = 4/0;
}catch (Exception e){
//2.在这里不手动指定回滚,数据状态还是修改前的 所以这里的回滚有必要吗?
transactionManager.rollback(status);
}
}
//后续可以自己去调用一下
我们日常开发中是不需要手动去调用spring操作事务的api,最常用到的就是@Transaction;我们经常会看到,很多人在某个方法上加个@Transaction注解用以告慰我们的灵魂
(当然有人甚至注解都不加,不加注解什么后果有想过吗?)但是我们是否想过一个简单的注解真的能达到我们的要求吗?会不会有发生该回滚的没回滚不该回滚的回滚了;还有我们加注解的位置对不对(加private修饰方法上呢?)
?我们在未加注解的方法中调用加了注解的方法是什么情况?这一系列情况我就不通过代码展示了,在这里直接给出结论
1.如上图第153行代码所示,改注解只能作用在public修饰符修饰的方法上
2.可在上图所示的TransactionInterceptor类中查看第118行代码可知,我们事务是通过aop环绕增强的且事务失效类是cglib动态代理类,所以在Spring的事务传播策略在内部方法调用时将不起作用
事务的传播特性
事务的传播特性主要通过代码演示;我们将UserService2_1中的方法称为主调方法,UserService2_2中的方法称为被调方法,PropagationTest中的方法为测试方法
REQUIRED
/**
*主调方法
*/
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
public void testREQUIRED(User user1,User user2){
try {
userDao.updateUserAge(user1);
userService22.testREQUIRED(user2);
}catch (Exception e){
}
}
/**
*被调方法
*/
@Transactional(propagation = Propagation.REQUIRED,isolation= Isolation.DEFAULT,rollbackFor = Exception.class)
public void testREQUIRED(User user){
userDao.updateUserAge(user);
int i = 1/0;
}
/**
*测试方法
*/
@Test
public void testREQUIRED(){
User user1 = new User();
user1.setId(1L);
user1.setAge(2);
User user2 = new User();
user2.setId(2L);
user2.setAge(2);
userService21.testREQUIRED(user1,user2);
}
上述代码我们分为三个步骤执行:
第一步
操作:将被调方法中的事务注解去掉,主调方法不变
结果:主调用方法不会回滚更新成功,被调方法也更新成功
结论: 一条sql过去就是一个事务,这就是不加注解的后果
第二步
操作:给被调方法加上注解,主调方法不变
结果:都会回滚而且抛出UnexpectedRollbackException:Transaction rolled back because it has been marked as rollback-only
疑问:明明在主调抓了异常为什么还会抛出这个异常呢?
结论: 因为事务两个公用了一个事务,被调用方法已经标志当前事务应该回滚
第三步
操作:被调方法的异常抓住,主调方法不变
结果:都更新成功
疑问:为啥?
结论: 注解都声明了异常回滚异常回滚,你都不抛异常回滚个锤子
结论
被调方法和主调方法都用REQUIRED修饰 那么就是两个方法在一个事务中执行,有一个回滚则都回滚,回滚时要显视的看到异常抛出
REQUIRES_NEW
/**
*主调方法
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void testREQUIRESNEW(User user1,User user2){
try {
userDao.updateUserAge(user1);
userService22.testREQUIRES_NEW(user2);
}catch (Exception e){
}
}
/**
*被调方法
*/
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation= Isolation.DEFAULT,rollbackFor = Exception.class)
public void testREQUIRES_NEW(User user){
userDao.updateUserAge(user);
int i = 1/0;
}
/**
*测试方法
*/
@Test
public void testREQUIRES_NEW(){
User user1 = new User();
user1.setId(1L);
user1.setAge(2);
User user2 = new User();
user2.setId(2L);
user2.setAge(2);
userService21.testREQUIRESNEW(user1,user2);
}
同样分为三个步骤执行:
第一步
操作:将被调方法中的事务注解去掉,主调方法不变
结果:主调用方法不会回滚更新成功,被调方法也更新成功
结论: 一条sql过去就是一个事务,这就是不加注解的后果
第二步
操作:给被调方法加上注解,主调方法不变
结果:主调方法更新成功 被调更新失败
疑问:为什么一个成功一个失败?
结论: 因为主调方法和被调方法开启了两个事物,互不影响;被调更新失败是因为异常回滚了
第三步
操作:被调方法的异常抓住,主调方法不变
结果:都更新成功
疑问:为啥?
结论: 注解都声明了异常回滚异常回滚,你都不抛异常回滚个锤子
结论
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
NESTED
/**
*主调方法
*/
@Transactional(propagation = Propagation.REQUIRED)
public void testNESTED(User user1,User user2){
try {
userDao.updateUserAge(user1);
userService22.testNESTED(user2);
}catch (Exception e){
}
}
/**
*被调方法
*/
@Transactional(propagation = Propagation.NESTED,isolation= Isolation.DEFAULT,rollbackFor = Exception.class)
public void testNESTED(User user){
userDao.updateUserAge(user);
int i = 1/0;
}
/**
*测试方法
*/
@Test
public void testNESTED(){
User user1 = new User();
user1.setId(1L);
user1.setAge(2);
User user2 = new User();
user2.setId(2L);
user2.setAge(2);
userService21.testNESTED(user1,user2);
}
同样分为三个步骤执行:
第一步
操作:将主调方法中的事务注解去掉,被调方法不变
结果:主调方法不会滚,被调方法回滚
结论: 如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED 且开启的事务相互独立,互不干扰(对于被动方法而言)
第二步
操作:主调方法开启事务,被调也开启;让主调方法抛出异常,被调方法抓住异常
结果:主调方法回滚,被调方法也回滚
结论: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,即作为当前事务的子事务,父事务回滚子事务也要回滚
第三步
操作:主调方法开启事务,被调也开启;让主调方法抓住异常,被调方法抛出异常
结果:主调方法不会滚,被调方法回滚
结论: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,即作为当前事务的子事务,子事务回滚父事务不用回滚
结论
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;
如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED 且开启的事务相互独立,互不干扰
MANDATORY
/**
*主调方法
*/
@Transactional(propagation = Propagation.REQUIRED)
public void testMANDATORY(User user1,User user2){
try {
userDao.updateUserAge(user1);
userService22.testMANDATORY(user2);
}catch (Exception e){
}
}
/**
*被调方法
*/
@Transactional(propagation = Propagation.MANDATORY,isolation= Isolation.DEFAULT,rollbackFor = Exception.class)
public void testMANDATORY(User user){
userDao.updateUserAge(user);
int i = 1/0;
}
/**
*测试方法
*/
@Test
public void testPROPAGATIONMANDATORY(){
User user1 = new User();
user1.setId(1L);
user1.setAge(2);
User user2 = new User();
user2.setId(2L);
user2.setAge(2);
userService21.testMANDATORY(user1,user2);
}
分两步骤执行:
第一步
操作:去掉主调方法的事务声明,被调方法异常抓住
结果:抛出异常 IllegalTransactionStateException:No existing transaction found for transaction marked with propagation 'mandatory'
结论: 主调不存在事务抛出异常
第二步
操作:给被调方法加上注解,被调方法异常抛出
结果:抛出异常UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only<br/>
疑问:为什么会抛出异常?
结论: 加入主调事务,当前事务已经被标记异常,提交时检测到,所以会抛出异常
结论
如果主调存在事务则加入,如果不存在则抛出异常
上面通过代码叙述了几种最常用的隔离级别,具体代码示例请移步 https://github.com/dogYin/resource/tree/master/spring