一个菜鸡JAVA后端的博客~

事务的隔离级别

事务的基本要素有:原子性、一致性、隔离性、持久性
由事务的隔离级别不同,并发会产生不同的并发问题。

  • 脏读
  • 不可重复读
  • 幻读

事务的隔离级别分为以下几类:

  • 读未提交(Read uncommitted):一个事务可以读取另一个未提交事务的数据
  • 读提交(Read committed):一个事务要等另一个事务提交后才能读取数据
  • 重复读(Repeatable read):在同一个事务内的查询都是事务开始时刻一致的
  • 序列化(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

在开始介绍隔离级别之前,先提一下丢失更新的概念。下面以存取款的方式来举例。

丢失更新

第一类丢失更新

时刻 事务1(小A) 事务2(小B)
T1 查询余额10000元 ---
T2 --- 查询余额10000元
T3 --- 消费1000元
T4 消费1000元 ---
T5 提交事务,余额9000元 ---
T6 --- 取消订单,回滚到T2时刻,余额10000元

其中,只有小A花费啦1000元,但是在T6时刻,余额却恢复到啦10000元。
这是由于两个事务并发,一个回滚一个提交导致打不一致。通常这种不一致称为第一类丢失更新。
不过大部分的主流数据库(如:Mysql和Oracle)都已经消除了此类丢失更新。

第二类丢失更新

时刻 事务1(小A) 事务2(小B)
T1 查询余额10000元 ---
T2 --- 查询余额10000元
T3 --- 消费1000元
T4 消费1000元 ---
T5 提交事务,余额9000元 ---
T6 --- 提交事务,根据之前的余额10000元减去1000元余下9000元

此过程中一共有着两笔交易,小A的和小B的,但是由于是在两个事务中,并不知道另外一个事务进行了怎样的操作。导致两笔事务提交后的余额都是9000元。这就是第二类丢失更新。

事务的隔离级别

为了克服事务之间的隔离级别,来不同程度上减少出现丢失更新的可能,数据库标准规范中定义了事务之间的隔离级别。

读未提交

读未提交是数据库的最低隔离级别,举个例子便于理解。

时刻 事务1(小A) 事务2(小B) 备注
T1 查询余额10000元 --- ---
T2 --- 查询余额10000元 ---
T3 --- 消费1000元,余下9000 ---
T4 消费1000元,余下8000 --- 读取到事务2未提交的9000
T5 提交事务 --- 余下8000
T6 --- 回滚事务 第一类丢失更新已被解决,余下8000

在这个场景中,T3时刻小B消费了1000元,因为此时的隔离级别是读未提交,所以在T4时刻小A消费时,读取到了小B未提交的事务2的数据,当在T5提交事务的时候,余额只剩下了8000元了。而在T6时刻由于小B的回滚,余额本应该是9000元的。这种错误的读取场景被称为脏读。

读提交

为了解决脏读,sql标准提出了第二个隔离级别:读/写提交。

时刻 事务1(小A) 事务2(小B) 备注
T1 查询余额10000元 --- ---
T2 --- 查询余额10000元 ---
T3 --- 消费1000元,余下9000 ---
T4 消费1000元,余下9000 --- 无法读取事务2未提交的数据,所以余额为9000
T5 提交事务 --- 余下9000
T6 --- 回滚事务 第一类丢失更新已被解决,余下9000

在T4时刻由于读提交的限制,事务1无法读取到事务2未提交的记录。只能读取到余额10000元,所以消费后为9000元,这样即使是事务2回滚了事务,余额也是正确的9000元。
这样虽然解决了脏读带来的问题,但是也引发了其他的问题。

时刻 事务1(小A) 事务2(小B) 备注
T1 查询余额10000元 --- ---
T2 --- 查询余额10000元 ---
T3 --- 消费1000元,余下9000 ---
T4 消费2000元,余下8000 --- 无法读取事务2未提交的数据,还是10000-2000
T5 --- 消费8000,余下1000 无法读取事务1未提交的数据,9000-1000
T6 --- 提交事务,余下1000 提交事务,余额变为1000
T7 提交事务时,余额只有1000了,不足以买单 --- 由于事务2已经提交,事务1会发现余额不足

在当前场景下,由于在小A开始消费的时候并不知道事务2中的消费是怎样的,而在T7时刻才知道事务2的提交结果,发现余额不足。这时候对于小A来说,他只知道钱莫名其妙的从T1时刻查询的10000元变成了1000元。对他来说账户余额是不能重复读取的,而是一个变化的值。这样的场景称之为不可重复读(unrepeatable read),这是读提交存在的问题。

可重复读

为了解决不可重复读带来的问题,sql标准提出了可重复读的隔离级别。此处,可重复读的概念是针对于数据库同一条记录而言的。可重复读会使得同一条数据库记录的读/写按照一个序列化进行操作,不会产生交叉的情况。这样就能保证同一条数据的一致性。
但是由于数据库常常会对多条记录进行读写,这时候就会产生幻读的情况。
下面以消费记录来举例子

时刻 事务1(小A) 事务2(小B) 备注
T1 --- 查询到10条消费记录,打印小票 ---
T2 开启一笔消费 --- ---
T3 提交事务 --- ---
T4 --- 打印出11条记录 由于在打印的过程中,小A多增加了一笔记录,所以打印出11条

在T1时刻,小B查询到10条记录,但是在T4之前的过程中,小A提交了一条事务,导致T4时刻打印出来的小票消费记录一共有11条。(可重复读针对同一条记录,此处针对的是多条记录,这是可重复读与幻读的区别所在)这样的场景叫做幻读。

序列化

为了解决幻读的问题,sql提出了序列化的隔离级别。它能够让sql按照顺序读写的方式,能够消除数据库事务之间并发产生数据不一致的问题。

各隔离级别的问题

隔离级别 脏读 不可重复读 幻读
读未提交
读提交 ×
可重复读 × ×
序列化 × × ×

隔离级别的选择

隔离级别越高,就越能够保证数据的正确性,但是会导致性能的直线下降。如最高级别序列化,会产生严重的并发问题,导致大量的线程被挂起,直到获得锁才能够进一步的操作,而恢复时又需要大量的等待时间。大部分场景下会选择读提交的方式来设置事务。不同数据库对隔离级别的默认值也不相同,其中Mysql的默认隔离级别是可重复读,Oracle的默认隔离级别是读提交。


NOTE: 文章若无特别说明均为原创文章,如果要转载请保留出处!
0 #SQL
分享