6.3 隔离级别
隔离级别解决的是,多个事务访问同一数据时出现的不一致的一系列问题。
6.3.1 数据库事务的知识
数据库事务具有以下4个基本特征:也就是著名的 ACID。
Atomic(原子性):事务中包含的操作被看作一个整体的业务单元,这个业务单元的所有操作要么全部成功,要么全部失败,不会出现部分成功、部分失败的场景。
Consistency(一致性):事务在完成时,必须使所有的数据都保持在一直的状态,在数据库中所有的修改都是基于事物的,保证了数据的完整性。
Isolation(隔离性):多个应用程序同时访问同一数据,这样数据库同样的数据就会在不同事务中被访问,这样就会产生丢失更新。为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上压制丢失更新的发生。因为互联网的应用常常是面对高并发的场景,所以隔离性是需要掌握的重点内容。
Durability(持久性):事务结束后,所有数据都会固化到一个地方,如保存在磁盘中,即使断电重启后也可以提供给应用程序访问。
第一类丢失更新
一个事物回滚另外一个事务提交而引发的数据不一致的情况,我们称之为第一类丢失更新。
发生原因两个事务是独立的,都从相同的数据源获取数据,彼此之间没有相互约束,就会造成访问修改相同的数据出错。如下场景。
时刻 | 事务1 | 事务2 |
---|---|---|
T1 | 初始库存100 | 初始库存100 |
T2 | 扣减库存,剩余99 | |
T3 | 扣减库存,剩余99 | |
T4 | 提交事务,库存变为99 | |
T5 | 回滚事务,库存100 |
不过现今,这种情况已经没有讨论价值了,因为目前主流的数据库都已经克服了第一类丢失更新问题。
第二类丢失更新
多个事务都提交引发的丢失更新被称之为第二类丢失更新。
解决第二类丢失更新的原则就是,事务之间不再是相互独立的,而是彼此之间的是有约束的,按约束等级不同,能解决的更新丢失的级别也会越高,最高级别的约束会完全防止丢失更新,但是付出的代价的就是性能的下降,所以要合理使用隔离级别。
6.3.2 详解隔离级别
为了压制丢失更新,我们需要使用隔离级别。隔离级别有四个:未提交读、读写提交、可重复读、串行化(按照由低到高的级别进行的排序)。
未提交读
级别:1
含义:允许一个事务去读取另一个事务未提交的数据的数据,造成的结果是:脏读。一个场景如下。
时刻 事务一 事务二 备注 T1 商品库存初始化为2 T2 读取库存为2 T3 扣减库存 库存为1 T4 扣减库存 库存为0,读取事务一未提交的库存数据1,扣减之后为0 T5 提交事务 库存保存0 T6 回滚事务 因为第一类丢失更新已经克服,所以不会回滚事务为2,库存为0,结果错误。 脏读是比较危险的隔离级别(由上表也可以看到,存储的数据出现了错误),所以实际中一般不用这个隔离级别。
读写提交
级别:2
含义:一个事务只能读取另外一个事务已经提交了的数据,不能读取未提交的数据。
克服脏读:
时刻 事务一 事务二 备注 T1 商品库存初始化为2 T2 读取库存为2 T3 扣减库存 库存为1 T4 扣减库存 库存为1,读取不到事务1未提交的库存数据,只能获得从数据库读到的2 T5 提交事务 库存保存为1 T6 回滚事务 因为第一类丢失更新已经克服,所以不会回滚事务为2,库存为1(就是事务二的操作结果),结果正确。 可以看到上面已经克服了脏读,但是读写提交,会发生不可重复读的情况,如下。
不可重复读:
时刻 事务一 事务二 备注 T1 商品库存初始化为1 T2 读取库存为1 T3 扣减库存 事务一未提交 T4 读取库存为1 事务二不能读取事务一未提交的0,所以只能读取数据库中的1,认为可扣减 T5 提交事务 事务一提交之后库存变为0,但是此时事务二还以为库存为1 T6 扣减库存 失败,因为此时库存为0,无法扣减.(可以看出扣减操作,不是扣减的读取到的临时值,而是操作的数据库中数据对应的地址),数据正确。 这种情况叫做不可重复度,但是可以看到虽然事务二的操作失败了,但是数据本身是正确的。产生的原因本质还是数据不同步而已。
可重复读
级别:3
含义:就是当其中一个事务在操作与自己要操作的相同数据的时候,由于加锁的缘故自己不被允许的读取该数据,所以可以等一会再尝试读取,这就叫做可重复读。解决了读写提交中的不可重复读的情况。
克服不可重复度:
时间 | 事务一 | 事务二 | 备注 |
---|---|---|---|
T0 | 商品初始化为1 | ||
T1 | 读取库存1 | ||
T2 | 扣减库存 | 事务1未提交 | |
T3 | 尝试读取库存 | 不允许读,等待事务1的提交 | |
T4 | 提交事务 | 库存变为0 | |
T5 | 读取库存 | 库存为0,无法扣减 |
可以看到,其实就是对多个事务操作相同的数据时,对该数据进行加锁,同一时刻只有先到的事务才能操作数据(读写)。但是这个级别也并不是完美的,其还会产生幻读,如下:
幻读:
时间 | 事务一 | 事务二 | 备注 |
---|---|---|---|
T0 | 读取库存50件 | 商品库存初始化为100,现在已经销售50笔,库存50笔 | |
T1 | 查询交易记录,50笔 | ||
T2 | 扣减库存 | ||
T3 | 插入1笔交易记录 | ||
T4 | 提交事务 | 库存49件,交易记录51件 | |
T5 | 打印交易记录,51笔 | 这里与查询不一致,在事务2看来有1笔是虚幻的,与之前查询的不一致。 |
这里的笔数不是数据库存储的值,而是一个统计值,商品库存则是数据库存储的值,这一点时要注意的。也就是说幻读不是针对一条数据库记录而言的,而是多条记录,例如,这51笔交易比数就是多条数据库记录统计出来的。而可重复度是针对数据库的但一条记录,例如,商品的库存是以数据库里面的一条记录存储的,他可以产生可重复度,而不能产生幻读。
- 串行化
级别:4(也就是最高级别)
含义:就是要求所有的 SQL 都按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以他能够完全保证数据的一致性。但是这样就会使性能严重下降。
使用合理的隔离级别
隔离级别和可能发生的现象
项目类型 脏读 不可重复读 幻读 未提交读 √ √ √ 读写提交 X √ √ 可重复度 X X √ 串行化 X X X
在现实中一般而言,选择隔离级别会议读写提交为主,它能够防止脏读,而不可避免不可重复读和幻读。为了克服数据不一致和性能问题,程序开发者还设计了乐观锁,甚至不再使用数据库而使用其他手段。例如,使用 Redis 作为数据载体。
对于隔离级别,不同的数据库至此也是不一样的。例如,Oracle 只能支持读写提交和串行化,而 MySQL 则支持上述四种,对于 Oracle 默认的隔离级别为读写提交,MySQL 则是可重复读,这些需要根据具体数据库来做决定。