五类并发问题
丢失更新(第一类丢失更新)
撤销一个事务时,把其他事务已提交的更新数据覆盖(A 和 B 事务并发执行,A 事务执行更新后,提交;B 事务在 A 事务更新后,B 事务结束前也做了对该行数据的更新操作,然后回滚,则两次更新操作都丢失了)。这种并发问题是由于完全没有隔离事务造成的。只要设置隔离级别,数据库就能保证此类问题不发生。
客户损失 100 元。
脏读
一个事务读到另一个事务未提交的更新数据(A 和 B 事务并发执行,B 事务执行更新后,A 事务查询 B 事务没有提交的数据,B 事务回滚,则 A 事务得到的数据不是数据库中的真实数据。也就是脏数据,即和数据库中不一致的数据)。
客户损失 100 元。
不可重复读
一个事务读到另一个事务已提交的更新数据(A 和 B 事务并发执行,A 事务查询数据,然后 B 事务更新该数据,A 再次查询该数据时,发现该数据变化了)。
覆盖更新(第二类丢失更新)
这是不可重复读中的特例,一个事务覆盖另一个事务已提交的更新数据(即 A 事务更新数据,然后 B 事务更新该数据,A 事务查询发现自己更新的数据变了)。
银行损失 100 元。
虚读(幻读)
一个事务读到另一个事务已提交的新插入的数据(A 和 B 事务并发执行,A 事务查询数据,B 事务插入或者删除数据,A 事务再次查询发现结果集中有以前没有的数据或者以前有的数据消失了)。
共享锁和排它锁
为了解决并发问题,数据库系统引入锁机制。
基本的封锁类型有两种: 排它锁 (Exclusive locks 简记为 X 锁) 和 共享锁 (Share locks 简记为 S 锁)。
- 排它锁又称为写锁。若事务 T 对数据对象 A 加上 X 锁,则只允许 T 读取和修改 A,其它任何事务都不能再对 A 加任何类型的锁,直到 T 释放 A 上的锁。这就保证了其它事务在 T 释放 A 上的锁之前不能再读取和修改 A。
- 共享锁又称为读锁。若事务 T 对数据对象 A 加上 S 锁,则事务 T 可以读 A 但不能修改 A,其它事务只能再对 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。这就保证了其它事务可以读 A,但在 T 释放 A 上的 S 锁之前不能对 A 做任何修改。
封锁粒度
数据库中为了实现并发控制而采用封锁技术。
封锁对象的大小称为封锁粒度(Granularity)。
封锁的对象可以是逻辑单元,也可以是物理单元。以关系数据库为例子,封锁对象可以是这样一些逻辑单元:属性值、属性值的集合、元组、关系、索引项、整个索引项直至整个数据库;也可以是这样的一些物理单元:页(数据页或索引页)、物理记录等。
封锁协议与隔离级别
在运用 排他锁 和 共享锁 对数据对象加锁时,还需要约定一些规则,例如何时申请 排他锁 或 共享锁、持锁时间、何时释放等。称这些规则为封锁协议(Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。不同的封锁协议对应不同的隔离级别。
一级封锁协议(对应 read uncommited)
一级封锁协议是:事务 T 在修改数据 R 之前必须先对其加 X 锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。
一级封锁协议可防止丢失更新,并保证事务 T 是可恢复的。
在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,所以它不能保证可重复读和不读 “脏” 数据。
二级封锁协议(对应 read commited)
二级封锁协议是:一级封锁协议加上事务 T 在读取数据 R 之前必须先对其加 S 锁,读完后即可释放 S 锁(瞬间 S 锁)。
二级封锁协议除防止了丢失更新,还可进一步防止读 “脏” 数据。
三级封锁协议(对应 reapetable read)
三级封锁协议是:一级封锁协议加上事务 T 在读取数据 R 之前必须先对其加 S 锁,直到事务结束才释放。
三级封锁协议除防止了丢失更新和不读‘脏’数据外,还进一步防止了不可重复读和覆盖更新。
四级封锁协议(对应 serialization)
四级封锁协议是对三级封锁协议的增强,其实现机制也最为简单,直接对 事务中 所 读取 或者 更改的数据所在的表加表锁,也就是说,其他事务不能 读写 该表中的任何数据。这样五类并发问题都得以避免!
注:封锁协议和隔离级别并不是严格对应的。
各种隔离级别所能避免的并发问题
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为 Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。