本篇文章主要针对MySQL的事务进行详细讲解,包括了事务管理、事务隔离级别、事务传播机制。
其中的事务隔离级别的案例解释和事务传播机制的参数解释中分别都给出了两个解释方案,建议大家可以将它们的两个解释方案结合起来理解。
事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消撤消之前到事务开始时的所以操作。
举个简单的例子理解以上四点
针对同一个事务
这个过程包含两个步骤
A: 800 - 200 = 600
B: 200 + 200 = 400
原子性表示,这两个步骤一起成功,或者一起失败,不能只发生其中一个动作
针对一个事务操作前与操作后的状态一致
操作前A:800,B:200
操作后A:600,B:400
一致性表示事务完成后,符合逻辑运算
表示事务结束后的数据不随着外界原因导致数据丢失
操作前A:800,B:200
操作后A:600,B:400
如果在操作前(事务还没有提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:800,B:200
如果在操作后(事务已经提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:600,B:400
针对多个用户同时操作,主要是排除其他事务对本次事务的影响
事务一)A向B转账200
事务二)C向B转账100
隔离级别自上而下级别越来越高,性能越来越低,隔离级别分别是:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
异常 | 解释 |
---|---|
脏读 | 脏读是指一个事务读取了另一个事务未提交的数据 |
不可重复读 | 不可重复读是指一个事务对同一数据的读取结果前后不一致。脏读和不可重复读的区别在于:前者读取的是事务未提交的脏数据,后者读取的是事务已经提交的数据,只不过因为数据被其他事务修改过导致前后两次读取的结果不一样 |
幻读 | 幻读是指事务读取某个范围的数据时,因为其他事务的操作导致前后两次读取的结果不一致。幻读和不可重复读的区别在于,不可重复读是针对确定的某一行数据而言,而幻读是针对不确定的多行数据。因而幻读通常出现在带有查询条件的范围查询中 |
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
例如:
张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。
与此同时,
事务B正在读取张三的工资,读取到张三的工资为8000。
随后,
事务A发生异常,而回滚了事务。张三的工资又回滚为5000。
最后,
事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
例如:
在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。
与此同时,
事务B把张三的工资改为8000,并提交了事务。
随后,
在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
例如:
目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。
此时,
事务B插入一条工资也为5000的记录。
这时,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。
提醒
不可重复读的重点是修改:
同样的条件,你读取过的数据,再次读取出来发现值不一样了
幻读的重点在于新增或者删除:
同样的条件,第 1 次和第 2 次读出来的记录数不一样
脏读指一个事务访问到了另一个事务未提交的数据,如下过程:
不可重复读指一个事务多次读取同一数据的过程中,数据值内容发生了改变,导致没有办法读到相同的值,描述的是针对同一条数据 update/delete 的现象,如下过程:
幻读指一个事务多次读取同一数据的过程中,全局数据(如数据行数)发生了改变,仿佛产生了幻觉,描述的是针对全表 insert/delete 的现象,如下过程:
或者是另一种场景,比如对于有唯一性约束的字段(如 id),发生如下过程:
这个是事务的默认取值,当ServiceA.methodA的事务级别是REQUIRED,ServiceB.methodB也是REQUIRED时,ServiceA.methodA起了一个事务,ServiceB.methodB被ServiceA.methodA调用,发现自己在事务中,就不再起事务了,如果ServiceA.methodA没有起事务,则ServiceB.methodB自己起事务。
当前存在事务中则加入当前事务,如果不在事务中则起一个新事务。
当ServiceA.methodA的事务级别是REQUIRED,ServiceB.methodB也是REQUIRES_NEW时,ServiceA.methodA起了一个事务,ServiceB.methodB被ServiceA.methodA调用,则挂起ServiceA.methodA的事务,自己重新起事务,这两个事务是隔离的,当ServiceB.methodB提交后,才继续ServiceA.methodA的事务。它与REQUIRED的事务区别在于事务的回滚程度,因为ServiceB.methodB是新起一个事务,那么就是存在两个不同的事务。如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB是不会回滚的。如果ServiceB.methodB失败回滚,它抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可以提交。
重新起事务,不管当前是否存在事务中,存在则挂起当前事务,重起事务,提交后继续完成被挂起的事务。事务相互隔离,回滚失败与否互不影响,但是抛出的异常外层事务是可以捕获的。
当标注的方法在事务中,则按事务的方式执行,此时类似标注REQUIRED,外层回滚一起回滚,其本身并不会发起事务,当外层没有事务,则按没有事务的方式执行。
如果当前在事务中,即以事务的形式运行,如果当前不在一个事务中,那么就以非事务的形式运行
当标注的方法在事务中,则按没有事务的方式执行,此时会挂起外层的事务,执行完后继续完成被挂起的事务,情况类似REQUIRES_NEW,都是隔离的,但是标注NOT_SUPPORTED是没有在事务中的,操作失败也不会回滚,但是异常能被外层事务捕获。
隔离外层事务,且本身不起事务,当外层有事务则挂起外层事务,执行完自己的方法后继续完成被挂起的事务,外层回滚不影响自身操作,但是抛出的异常外层事务是可以捕获的。
必须在一个事务中运行。也就是说,只能被一个父事务调用。否则,就要抛出异常。
当ServiceA.methodA的事务级别是REQUIRED,ServiceB.methodB也是NEVER时,ServiceA.methodA起了一个事务,ServiceB.methodB被ServiceA.methodA调用,ServiceB.methodB就会抛出异常
不能在事务中运行。否则,自身就会抛出异常。
这个级别和REQUIRES_NEW理解基本一样,不同在于NESTED不是重新起一个事务,而是建一个savepoint,提交时间要和外层事务一起提交,一起回滚,但是有一个好处在于它存在一个savepoint。例如自身回滚失败,外层可以选择新分支执行并尝试完成自己的事务。
开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.
这里我们将会通过以下几个类去演示(省略接口层)
UserService
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UsersMapper usersMapper;
@Override
public void saveParent() {
Users user = new Users();
user.setId("1");
user.setUsername("parent");
usersMapper.insert(user);
}
@Override
public void saveChildren() {
saveChildren1();
//模拟异常
int i = 1 / 0;
saveChildren2();
}
public void saveChildren1(){
Users user = new Users();
user.setId("2");
user.setUsername("children1");
usersMapper.insert(user);
}
public void saveChildren2(){
Users user = new Users();
user.setId("3");
user.setUsername("children2");
usersMapper.insert(user);
}
}
TestTransService
@Service
public class TestTransServiceImpl implements TestTransService {
@Autowired
private UserService userService;
@Override
public void testTrans() {
userService.saveParent();
userService.saveChildren();
}
}
单元测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserTest {
@Autowired
private TestTransService transService;
@Test
public void test(){
transService.testTrans();
}
}
测试场景在类TestTransService的方法中调用另一个类的方法
方法执行顺序为先保存parent,再保存children1,再保存children2
首先我们在方法上不加@Transaction注解,saveChildren中模拟异常情况
执行报错
数据库中
发现发生异常没有回滚,children1依然保存成功
接下来我们重点演示事务的不同传播级别(注意:每次演示后都会清空数据库)
首先我们在上层方法上加入事务注解
下层被调用方法上不加
运行测试 报错 java.lang.ArithmeticException: / by zero
此时数据库中没有新增数据证明事务生效回滚
说明上层方法开启事务,事务隔离级别为required的情况下,底层方法同样会加入该事务
屏蔽上层事务
开启下层savechildren方法事务
运行测试,发现这次parent保存成功
说明事务隔离级别为required的情况下,如果当前没有事务,则自己创建一个新的事务.
上层方法和下层savechildren方法都加上事务注解
运行后和情况1一样数据库中没有存入数据
说明务隔离级别为required的情况下,如果当前存在事务,则会加入上层调用这个事务.成为一个整体事务
通俗点来说:
required情况下,父母有饭吃,会给孩子吃,父母没饭吃,孩子有饭自己吃,父母和孩子都有饭吃,孩子会跟父母一起吃(这里的饭就是事务)
底层方法上加入事务,事务传播行为设置成supports
上层方法屏蔽事务
运行后数据库中存在数据如下
说明底层被调用方法事务为supports的情况下,跟随上层方法,如果当前没有事务,则底层方法也不执行事务
注意如果上层方法事务为supports,相当于当前没有事务,所以底层也不执行事务
底层方法不变,上层方法开启事务,并设置为required
执行后
说明底层被调用方法事务为supports的情况下,如果当前存在事务,则底层方法加入该事务.
通俗点来说:
supports情况下,父母有饭吃,孩子就有饭吃,父母没饭吃,孩子也没饭吃
类似于supports,但是如果上层调用方法当前没有事务 ,则会抛出异常
如下,上层屏蔽事务
底层方法开启事务并设置成mandatory
执行后控制台报错
说明底层被调用方法事务为mandatory的情况下,如果当前不存在事务,则会报错.
通俗点来说:
mandatory情况下,父母有饭吃,孩子就有饭吃,父母没饭吃,孩子不干了会大声哭
上层调用方法开启事务 设置传播级别为required
下层开启事务,设置为requires_new
运行后 数据库没有数据 好像和requried一样,都是执行的当前事务,但是实际上不是这样的我们接着看场景2
我们在上层方法中新增异常模拟
下层方法中屏蔽掉异常模拟
按照之前requried的经验,此时如果在同一个事务中,数据库里应该没有数据
实际上执行后数据库如下
我们发现saveParent方法回滚了,但是底层saveChildren方法没有回滚
说明,事务级别为requires_new的情况下,如果当前存在的事务,则挂起事务,自己创建一个新的事务给自己使用,如果当前没有事务,则和required一致.
通俗点来说:
requires_new情况下,父母有饭吃,孩子也不吃父母的饭,只吃自己的饭
上层方法不开启事务
下层方法开启事务并设置为NOT_SUPPORTED
执行结果如下
和没开启事务一样
底层方法不变,上层方法开启事务
执行后发现saveParent方法回滚,但是saveChildren方法没有回滚
说明,事务级别为not_supported的情况下,不管当前是否存在事务,都会挂起事务,不进行事务.
通俗点来说:
not_supported情况下,父母不管有没有饭吃,孩子都不吃饭
上面的代码不变,改变底层方法的传播级别为never
执行控制台会报错
说明,事务级别为never的情况下,以非事务方式执行,如果当前存在事务,则抛出异常
通俗点来说:
never情况下,父母有饭吃,给孩子吃,孩子不吃还要哭
上层方法模拟异常并开启事务
下层方法设置事务为NESTED
执行结果
这里感觉和required是不是很像 我们接着看
这回把模拟异常放在底层方法上
执行
结果还是没有数据,结果也一致,难道真的和required一样,都加入到一个事务中吗.我们接下来看
我们在上层用tay catch捕获底层方法的异常
执行
发现底层方法回滚了,但是上层方法没有回滚.
再看看如果这种场景下我们将底层方法事务设置成required
执行后
上层和下层方法都回滚了,
所以说明 Nested实际上和required不一样,required是将下层被调用方法加入到当前事务中,而nested相当于和当前事务不是一个事务了,但是和required_new不一样,如果上层当前事务发生回滚,底层会跟着回滚,而如果下层出现异常回滚,上层事务可以通过catch底层方法的异常控制自己不回滚,这是他们最大的区别
通俗点来说:
nested相当于,领导犯错了,下属会跟着一起受罚,但是下属犯错了,领导可以选择到底跟不跟下属一起受罚.