前言
如果数据库中的事务都是串行执行的,这种方式可以保障事务的执行不会出现异常和错误,但带来的问题是串行执行会带来性能瓶颈;而事务并发执行,如果不加以控制则会引发诸多问题,包括死锁、更新丢失等等。这就需要我们在性能和安全之间做出合理的权衡,使用适当的并发控制机制保障并发事务的执行。
并发事务带来的问题
首先我们先来了解一下并发事务会带来哪些问题。并发事务访问相同记录大致可归纳为以下3种情况:
- 读-读:即并发事务相继读取同一记录;
- 写-写:即并发事务相继对同一记录做出修改;
- 写-读或读-写:即两个并发事务对同一记录分别进行读操作和写操作。
读-读
因为读取记录并不会对记录造成任何影响,所以同个事务并发读取同一记录也就不存在任何安全问题,所以允许这种操作。
写-写
如果允许并发事务都读取同一记录,并相继基于旧值对这一记录做出修改,那么就会出现前一个事务所做的修改被后面事务的修改覆盖,即出现提交覆盖的问题。
另外一种情况,并发事务相继对同一记录做出修改,其中一个事务提交之后之后另一个事务发生回滚,这样就会出现已提交的修改因为回滚而丢失的问题,即回滚覆盖问题。
这两种问题都造成丢失更新,其中回滚覆盖称为第一类丢失更新问题,提交覆盖称为第二类丢失更新问题。
写-读或读-写
这种情况较为复杂,也最容易出现问题。
如果一个事务读取了另一个事务尚未提交的修改记录,那么就出现了脏读的问题;
如果我们加以控制使得一个事务只能读取其他已提交事务的修改的数据,那么这个事务在另一事物提交修改前后读取到的数据是不一样的,这就意味着发生了不可重复读;
如果一个事务根据一些条件查询到一些记录,之后另一事物向表中插入了一些记录,原先的事务以相同条件再次查询时发现得到的结果跟第一次查询得到的结果不一致,这就意味着发生了幻读。
事务的隔离级别
对于以上提到的并发事务执行过程中可能出现的问题,其严重性也是不一样的,我们可以按照问题的严重程度排个序:
丢失更新 > 脏读 > 不可重复读 > 幻读
因此如果我们可以容忍一些严重程度较轻的问题,我们就能获取一些性能上的提升。于是便有了事务的四种隔离级别:
- 读未提交(Read Uncommitted):允许读取未提交的记录,会发生脏读、不可重复读、幻读;
- 读已提交(Read Committed):只允许读物已提交的记录,不会发生脏读,但会出现重复读、幻读;
- 可重复读(Repeatable Read):不会发生脏读和不可重复读的问题,但会发生幻读问题;但MySQL在此隔离级别下利用间隙锁可以禁止幻读问题的发生;
- 可串行化(Serializable):即事务串行执行,以上各种问题自然也就都不会发生。
值得注意的是以上四种隔离级别都不会出现回滚覆盖的问题,但是提交覆盖的问题对于MySQL来说,在Read Uncommitted、Read Committed以及Repeatable Read这三种隔离级别下都会发生(标准的Repeatable Read隔离级别不允许出现提交覆盖的问题),需要额外加锁来避免此问题。
隔离级别的实现
SQL规范定义了以上四种隔离级别,但是并没有给出如何实现四种隔离级别,因此不同数据库的实现方式和使用方式也并不相同。而SQL隔离级别的标准是依据基于锁的实现方式来制定的,因为有必要先了解一下传统的基于锁的隔离级别是如何实现的。
传统隔离级别的实现
既然说到传统的隔离级别是基于锁实现的,我们先来了解一下锁。
锁
传统的锁有两种:
- 共享锁(Shared Locks):简称S锁,事务对一条记录进行读操作时,需要先获取该记录的共享锁。
- 排他锁(Exclusive Locks):简称X锁,事务对一条记录进行写操作时,需要先获