锁的类型
InnoDB存储引擎实现了如下两种标准的行级锁:
- 共享锁(SLock),允许事务读一行数据。
- 排他锁(XLock),允许事务删除或更新一行数据。
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
此外,InnoDB存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的
锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持
一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层
次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁。
一致性非锁定读
致性的非锁定读(consistent nonlocking read)是指InnoDB存储引繁通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。
快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不要上锁的,因为没有事务需要对历史的数据进行修改操作。可以看到,非锁定读机制极大地提髙了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。
但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。在事务隔离级别READ COMMITTED和REPEATABLE READ (InnoDB存储引擎的默认事务隔离级別)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别
下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
MVCC的实现过程
版本号
系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
事务版本号:事务开始时的系统版本号。
隐藏的列
MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
创建版本号:指示创建一个数据行的快照时的系统版本号;
删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
以下实现过程针对可重复读隔离级别。
当开始一个事务时,该事务的版本号肯定大于当前所有数据行快照的创建版本号,理解这一点很关键。数据行快照的创建版本号是创建数据行快照时的系统版本号,系统版本号随着创建事务而递增,因此新创建一个事务时,这个事务的系统版本号比之前的系统版本号都大,也就是比所有数据行快照的创建版本号都大。
- SELECT
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于等于 T 的版本号,因为如果大于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T 所要读取的数据行快照的删除版本号必须是未定义或者大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
INSERT
将当前系统版本号作为数据行快照的创建版本号。DELETE
将当前系统版本号作为数据行快照的删除版本号。UPDATE
将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。
一致性锁定读
InnoDB存储引擎对于select支持两种一致性的锁定读(locking read)操作:
- SELECT — FOR UPDATE
- SELECT — LOCK IN SHARE MODE
SELECT-FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。SELECT-LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被阻塞。
锁的算法
行锁的三种算法
InnoDB存储引擎有3种行锁的算法,其分别是:
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock : Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。Next-Key Lock 是结合了 Gap Lock 和 Record Lock 的一种锁定算法,在 Next-Key Lock算法下,InnoDB对于行的査询都是采用这种锁定算法。
例如一个索引有10, 11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:
(-00 ,10]
(10,11]
(11, 13]
(13, 20]
(20,+ ~)
当査询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。(仅适用于查询的列是唯一索引的情况)
若唯一索引由多个列组成,而査询仅是査找多个唯一索引列中的其中一个,那么査询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定。
幻读问题
Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。
InnoDB存储引擎默认的事务隔离级别是REPEATABLE READ,在该隔离级别下,其采用Next-Key Locking的方式来加锁。而在事务隔离级别READ COMMITTED下,其仅采用Record Lock。
事务并发一致性问题
丢失修改
T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。
读脏数据
T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
不可重复读
T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
幻影读
T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
事务隔离级别
未提交读(READ UNCOMMITTED) —事务中的修改,即使没有提交,对其它事务也是可见的。
提交读(READ COMMITTED) — 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
可重复读(REPEATABLE READ)—保证在同一个事务中多次读取同样数据的结果是一样的。
可串行化(SERIALIZABLE)–强制事务串行执行。
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
未提交读 | √ | √ | √ |
提交读 | × | √ | √ |
可重复读 | × | × | √ |
可串行化 | × | × | × |
阻塞
因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行。
在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来控制等待的时间(默认是50秒),innodb_rollback_on_timeout用来设定是否在等待超时时对进行中的事务进行回滚操作(默认是OFF,代表不回滚)。
在默认情况下InnoDB存储引擎不会回滚超时引发的错误异常。其实InnoDB存储引擎在大部分情况下都不会对异常进行回滚。
锁升级
锁升级(Lock Escalation)是指将当前锁的粒度降低。举例来说,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁。如果在数据库的设计中认为锁是一种稀有资源,而且想避免锁的开销,那数据库中会频繁出现锁升级现象。
InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相
反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一
个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。
MySQL技术内幕(InnoDB存储引擎)