事务的基本要素有:原子性、一致性、隔离性、持久性
由事务的隔离级别不同,并发会产生不同的并发问题。
- 脏读
- 不可重复读
- 幻读
事务的隔离级别分为以下几类:
- 读未提交(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的默认隔离级别是读提交。