MySql - 数据库的脏读、不可重复读、幻读、两类丢失更新与四大隔离级别
1.什么是脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新?
2.为什么会产生脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新?
3.如何防止脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新的发生?
4.如何设置数据库隔离级别?
---------------------------------------------------------------------------------------------------------------------------
1.什么是脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新?
脏读(dirty read):B事务读取到A事务尚未提交的更改过的数据,并在这个数据的基础上进行了操作。如果A事务回滚,那么B事务读到的数据就是脏数据,此现象称为脏读。
不可重复读(unrepeatable read):B事务读取了A事务已经提交过的更改(或删除)的数据。如B事务第一次读取数据,然后A事务更改该数据并提交,B事务再次读取数据,导致两次读取的数据不一致,此现象称为不可重复读。
幻读(phantom read):B事务读取了A事务已经提交的新增数据。
第一类丢失更新: A事务撤销事务,覆盖了B事务提交的事务(现代关系型数据库中已经不会发生)。
第二类丢失更新: A事务提交事务,覆盖了B事务提交的事务(是不可重复读的特殊情况)。
其中不可重复读和幻读容易混淆,不可重复读侧重于数据的修改,幻读侧重于数据的新增或删除。针对不可重复读,一般采取行级锁机制,防止读到更改的数据。针对幻象读,一般采用表级锁机制,将整个表锁住,防止新增数据行。
2.为什么会产生脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新?
2.1 第一类丢失更新
如下两个事务,事务A先开启事务查询账户有1000元,然后准备存款500元,使其账户变为1500,在此事务进行的同时,事务B进行转账操作,并提交了事务,使账户金额变为500,而事务A最后因为某些原因,没有提交事务,而是回滚了事务,将账户金额重新设置为1000。但实际上,账户已经被转走了500元,这就是第一类丢失更新产生的原因。
时间序号 | 事务A | 事务B |
T1 | 开启事务 | 开启事务 |
T2 | 查询账户余额 balance=1000 | 查询账户余额 balance=1000 |
T3 | 存入500 balance=1500 | |
T4 | 转出500 balance=500 | |
T5 | 提交事务 balance=500 | |
T6 | 取消事务,回滚 balance=1000 |
2.2 脏读
如下两个事务,事务A先开启事务查询账户有1000元,然后准备转出500元,使其账户变为500,在此事务进行的同时,事务B进行了查询操作,查询到了事务A未提交的余额500元。在事务B刚刚查询后,A事务进行了回滚,实际账户余额回复为1000元,这时事务B用错误的余额数据500元进行转入操作,并提交了事务,使账户金额变为600。但实际上,账户余额应该为1100元,这就是脏读产生的原因。
时间序号 | 事务A | 事务B |
T1 | 开启事务 | 开启事务 |
T2 | 查询账户余额 balance=1000 | |
T3 | 转出500 balance=500 | |
T4 | 查询账户余额 balance=500 | |
T5 | 取消事务,回滚 balance=1000 | |
T6 | 转入100 balance=600 | |
T7 | 提交事务 balance=600 |
2.3 不可重复读
如下两个事务,事务A先开启事务查询账户有1000元,在此事务进行的同时,事务B也进行了查询操作,查询到账户余额为1000元。然后事务A进行了转出操作,转出了100元,余额变成了900元,并提交了事务。这时当事务B再次查询余额的时候,发现账户余额变成了900元,这就是不可重复读产生的原因。当初因为对并发事务不了解,以为事务B两次查到的数据不一致没什么影响,后来仔细想想才发现,事务B一直都没有提交,这就会产生事务B业务逻辑的失效。
时间序号 | 事务A | 事务B |
T1 | 开启事务 | 开启事务 |
T2 | 查询账户余额 balance=1000 | |
T3 | 查询账户余额 balance=1000 | |
T4 | 转出100 balance=900 | |
T5 | 提交事务 | |
T6 | 查询账户余额 balance=900 | |
T7 | 提交事务 |
2.4 第二类丢失更新
理解了不可重复读出现的原因后,如果把上述例子换成事务A和事务B同时进行转出操作,就是第二类丢失更新产生的原因了,因为多个事务同时对一行数据进行更新导致的问题。
时间序号 | 事务A | 事务B |
T1 | 开启事务 | 开启事务 |
T2 | 查询账户余额 balance=1000 | |
T3 | 查询账户余额 balance=1000 | |
T4 | 转出100 balance=900 | |
T5 | 转出100 balance=900 | |
T6 | 提交事务 balance=900 | |
T7 | 提交事务 balance=900 |
2.5 幻读
A事务读取到B事务提交的新增数据,就像产生了幻觉一样,所以称作幻读,幻读一般发生在数据统计事务中。
时间序号 | 事务A | 事务B |
T1 | 开启事务 | 开启事务 |
T2 | 统计存款总金额为1000元 | |
T3 | 新增存款金额100元 | |
T4 | 提交事务 | |
T5 | 再次统计存款总金额为1100元 | |
T6 | 提交事务 |
3.如何防止脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新的发生?
为了解决上面提及的并发问题,主流关系型数据库都会提供四种事务隔离级别。
3.1 读未提交(Read Uncommitted)
该隔离级别所有事务都可以看到其他未提交事务的执行结果。本隔离级别是最低的隔离级别,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用。因为采用这种隔离级别只能防止第一类更新丢失问题,不能解决脏读,不可重复读及幻读问题。
3.2 读已提交(Read Committed)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别可以防止脏读问题,但会出现不可重复读及幻读问题。
3.3 可重复读(Repeatable Read)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。这种隔离级别可以防止除幻读外的其他问题。
3.4 可串行化(Serializable)
最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读、第二类更新丢失问题。在这个级别,可以解决上面提到的所有并发问题,但可能导致大量的超时现象和锁竞争,通常数据库不会用这个隔离级别,我们需要其他的机制来解决这些问题:比如乐观锁和悲观锁机制(下篇再详细分析)。
列个表格总结下并发一致性问题和四大隔离级别的关系
隔离级别 | 脏读 | 不可重复读 | 幻读 | 第一类丢失更新 | 第二类丢失更新 |
读未提交 | 不可预防 | 不可预防 | 不可预防 | 可以预防 | 不可预防 |
读已提交 | 可以预防 | 不可预防 | 不可预防 | 可以预防 | 不可预防 |
可重复读 | 可以预防 | 可以预防 | 不可预防 | 可以预防 | 可以预防 |
可串行化 | 可以预防 | 可以预防 | 可以预防 | 可以预防 | 可以预防 |
4.如何设置数据库隔离级别?
设置全局隔离级别:
set global transaction isolation level READ UNCOMMITTED;//读未提交
set global transaction isolation level READ COMMITTED;//读已提交
set global transaction isolation level REPEATABLE READ;//可重复读
set global transaction isolation level SERIALIZABLE;//串行化
设置session级别的隔离级别:
set session transaction isolation level READ UNCOMMITTED;//未提交读
set session transaction isolation level READ COMMITTED;//已提交度
set session transaction isolation level REPEATABLE READ;//可重复读
set session transaction isolation level SERIALIZABLE;//串行化
Spring事务:
Spring事务默认使用数据库的隔离级别,当然也可以通过@Transactional中的isolation参数调整当前Session级的隔离级别。
solation的参数有以下五种:
(1)solation.DEFAULT:为数据源的默认隔离级别
(2)isolation=Isolation.READ_UNCOMMITTED:未授权读取级别
(3)iIsolation.READ_COMMITTED:授权读取级别
(4)iIsolation.REPEATABLE_READ:可重复读取级别
(5)iIsolation.SERIALIZABLE:序列化级别