目录
1.什么是脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新?
2.为什么会产生脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新?
2.1 第一类丢失更新
2.2 脏读
2.3 不可重复读
2.4 第二类丢失更新
2.5 幻读
3.如何防止脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新的发生?
3.1 读未提交(Read Uncommitted)
3.2 读已提交(Read Committed)
3.3 可重复读(Repeatable Read)
3.4 可串行化(Serializable)
4.如何设置数据库隔离级别?
1.什么是脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新?
脏读:B事务读取到A事务尚未提交的更改过的数据,并在这个数据的基础上进行了操作。如果A事务回滚,那么B事务读到的数据就是脏数据,此现象称为脏读。
不可重复读:B事务读取了A事务已经提交过的更改(或删除)的数据。如B事务第一次读取数据,然后A事务更改该数据并提交,B事务再次读取数据,导致两次读取的数据不一致,此现象称为不可重复读。
幻读: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 |
总结:A事务update、B事务update同一条数据,A事务回滚
2.2 脏读
如下两个事务,A事务先查询账户内有1000元,转出500元后,同时B事务查询账户余额,查询到了事务A未提交的余额500元。此时A事务进行了回滚,则B事务所读取的数据就是不正确的,即为脏读。
时间序号 | A事务 | B事务 |
T1 | 开启事务 | 开启事务 |
T2 | 查询账户余额 balance=1000 | |
T3 | 转出500 balance=500 | |
T4 | 查询账户余额 balance=500 | |
T5 | 回滚事务 balance=1000 |
总结:A事务update、B事务select同一条数据、A事务回滚
2.3 不可重复读
如下两个事务,A、B开启事务,事务B查询账户余额为1000元。A事务转出了100元,余额变成了900元,并提交了事务。这时当事务B再次查询余额的时候,发现账户余额变成了900元,这就是不可重复读产生的原因。
时间序号 | A事务 | B事务 |
T1 | 开启事务 | 开启事务 |
T2 | 查询账户余额 balance=1000 | |
T3 | 转出100 balance=900 | |
T4 | 提交事务 | |
T5 | 查询账户余额 balance=900 |
总结:B事务select、A事务update并提交事务、B事务再次select
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 |
总结:A事务select、B事务update并提交事务、A事务也update并提交事务
2.5 幻读
A事务读取到B事务提交的新增数据,就像产生了幻觉一样,所以称作幻读,幻读一般发生在数据统计事务中。
时间序号 | A事务 | B事务 |
T1 | 开启事务 | 开启事务 |
T2 | 统计存款笔数为10笔 | |
T3 | 新增存款1笔 | |
T4 | 提交事务 | |
T5 | 再次统计存款笔数为11笔 |
总结:A事务select count(*)、B事务insert、A事务再次select count(*)
3.如何防止脏读、不可重复读、幻读、第一类丢失更新和第二类丢失更新的发生?
为了解决上面提及的并发问题,主流关系型数据库都会提供四种事务隔离级别。
3.1 读未提交(Read Uncommitted)
该隔离级别所有事务都可以看到其他未提交事务的执行结果。本隔离级别是最低的隔离级别,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用。因为采用这种隔离级别只能防止第一类更新丢失问题,不能解决脏读,不可重复读及幻读问题。
3.2 读已提交(Read Committed)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别可以防止脏读问题,但会出现不可重复读及幻读问题。
3.3 可重复读(Repeatable Read)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。这种隔离级别可以防止除幻读外的其他问题。
3.4 可串行化(Serializable)
最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读、第二类更新丢失问题。在这个级别,可以解决上面提到的所有并发问题,但可能导致大量的超时现象和锁竞争。最直观的体现就是,当数据库隔离级别设置为串行化后,A事务在未提交之前,B事务对A事务数据的操作都会被阻塞。通常数据库不会用这个隔离级别,我们需要其他的机制来解决这些问题:比如乐观锁和悲观锁机制(下篇再详细分析)。列个表格总结下并发一致性问题和四大隔离级别的关系
隔离级别 | 脏读 | 不可重复读 | 幻读 | 第二类丢失更新 |
读未提交 | 不可预防 | 不可预防 | 不可预防 | 不可预防 |
读已提交 | 可以预防 | 不可预防 | 不可预防 | 不可预防 |
可重复读 | 可以预防 | 可以预防 | 不可预防 | 可以预防 |
可串行化 | 可以预防 | 可以预防 | 可以预防 | 可以预防 |
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级的隔离级别(数据库的隔离级别个人感觉粒度比较粗,Spring的隔离级别好一些 ,因为可以定义到方法上)。
solation的参数有以下五种:
(1)solation.DEFAULT:为数据源的默认隔离级别
(2)isolation=Isolation.READ_UNCOMMITTED:未授权读取级别
(3)iIsolation.READ_COMMITTED:授权读取级别
(4)iIsolation.REPEATABLE_READ:可重复读取级别
(5)iIsolation.SERIALIZABLE:序列化级别