InnoDB存储引擎之数据库锁

  锁的目的就是为了支持对共享资源的并发访问,并提供数据的完整性和一致性,InnoDB具有两种标准的行级锁:共享锁(S Lock)和排他锁(X Lock)。
  不同的数据库,甚至MySQL不同的存储引擎,对锁的实现都是完全不同的。MyISAM是表级锁设计,在并发修改的时候性能较差;SQL Server的老版本使用页锁,新版本开始支持乐观并发和悲观并发,而且在乐观并发下开始支持行级锁了;InnoDB是通过在每个页中采用位图的形式进行锁管理的,所以InnoDB中锁是一种很轻量级的实现,即使同时锁住再多的记录其开销几乎也不会发生什么变化。

1. 一致性非锁定读

  一致性非锁定读是指InnoDB通过行多版本控制(MVCC)的方式来读取当前执行时间下数据库中的行数据,比如读取的行正进行DELETE或UPDATE操作,此时的读操作不会等待该行上X锁释放,而是会去读取行的某个快照数据,快照数据是通过undo段来完成的,undo数据是事务中用来回滚时候使用的,所以只要执行事务产生就肯定会产生undo数据,使用快照数据是没有额外开销的,同时快照是不会被事务修改的,所以访问这些数据也不用进行加锁操作。
  InnoDB对每一行记录可能会有多个版本的快照,因此称之为行多版本技术,这种技术下的并发控制成为多版本并发控制(MVCC)。在READ COMMITTED和REPEATABLE READ事务隔离级别下,InnoDB会使用一致性非锁定读,不过不同的是在READ COMMITTED事务隔离级别下,对快照数据一致性非锁定读总是读取被锁定数据的最新一份快照数据;而在REPEATABLE READ事务隔离级别下,对于快照数据非锁定一致性读总是读取事务开始时候行数据版本。

2. 一致性锁定读

  某些情况下用户需要显式对数据库读取操作进行加锁操作以保证数据逻辑的一致性,这可以通过加锁语句来实现:

SELECT … FOR UPDATE;
SELECT … LOCK IN SHARE MODE;

  这些语句必须在事务中才能使用,在事务提交的时候锁也就自动被释放了。注意的是即使行记录被这样显式锁定了,但其他事务一致性非锁定读也是可以读取某个版本的快照数据的,锁之间的相互作用只有使用上面显示语句才会作用。
  对于一个外键列,如果没有显式对该列添加索引,则InnoDB会自动对其添加一个索引,这样可以避免表锁。在需要对外键值进行插入或更新就首先需要SELECT父表中的记录,此时的SELECT不能使用一致性非锁定读的方式,因为可能会导致数据不一致的问题,此时实质上使用的是SELECT … LOCK IN SHARE MODE,即会主动加一个S锁防止事务执行中该行被删除或修改,这里也表明对外键添加索引是很有必要性的。

3. InnoDB锁算法

  InnoDB有3种行锁算法
  (a). Record Lock(记录锁) - 单个行记录上的锁;
  (b). Gap Lock(间隙锁) - 锁定一个范围但不包含记录本身;
  (c). Next-Key Lock - 其实是Gap Lock + Record Lock的组合,锁定一个范围并且锁定记录本身,这种范围的锁定集合方式表示就是左开右闭;
  Record Lock总是会去锁定索引记录,如果InnoDB在建表时候没有设置任何一个索引,那么InnoDB此时会使用隐式的主键来锁定记录。
  当查询的索引含有唯一属性的时候,InnoDB会对Next-Key Lock进行优化降级为Record Lock,即仅锁住索引本身就可以了,应用的并发性得到很好的提高;不过如果唯一索引是由多个列产生的组合索引,而查找仅仅是部分列,那么该查询本质是Range类型的查询,此时InnoDB还是会使用Next-Key Lock进行锁定而不会降级为Record Lock。举例来说,比如对于值为1、2、5的主键,如果对5进行SELECT … FOR UPDATE锁定,此时是可以立即插入4的,因为这里的使用Record Lock只锁定记录5,而不是Next-Key Lock锁定(2,5]这个范围。
  对于有辅助索引的情况,比如对于主键a、辅助索引b构成的表,其中的记录有:(1,1) (3,1) (5,3) (7,6) (10,8),如果执行条件b=3的SELECT … FOR UPDATE,则根据Next-Key Locking的技术加锁,需要对主键和辅助索引都进行锁定:主键索引只使用Record Lock锁定a=5的行,辅助索引会执行(1,3)的Next-Key Lock,而且InnoDB还会对辅助索引的下一个键值(3,6)加上Gap Lock,因此对于插入a=5、b in (1,6)的操作都会被阻塞。执行(1,6)的Gap Lock是为了阻止多个事务将记录插入到同一范围内,导致Phantom Problem的产生,可以将事务隔离级别设置为READ COMMITTED来显式地关闭Gap Lock,在这种事务隔离级别下就会使用Record Lock了。

4. Phantom读

  Phantom Problem也叫不可重复读,指在同一事务下多次读取同一数据集合,如果这个事务还没结束的时候其它事务访问同一数据集合并执行DML操作,就会本事务先后执行两次同样的SQL语句可能返回不同的结果,第二次执行SQL语句可能返回之前不存在的行或者少于第一次返回的行。

5. 程序逻辑上的丢失更新

  这主要是很多程序会从数据库中检索记录,接着进行业务逻辑判断,最后再更新这条记录,即业务逻辑的插入导致了查询和更新无法在一步原子执行,那么很有可能两个事务交替执行,导致后一个事务覆盖掉前一事务的操作接口。
  所以对一致性要求高的操作,查询记录的时候需要在事务中使用SELECT … FOR UPDATE施加一个X锁,让检索-操作-更新该条记录的时候其他事务处于阻塞等待状态。

6. 死锁

  解决死锁问题最简单的方法是使用超时机制:当两个事务互相等待的时候,首先超时的事务执行回滚,而另外一个事务就能够得到资源得以继续执行了,InnoDB中是通过设置参数innodb_lock_wait_timeout来设置(默认50s)超时时长的。
  超时机制采用的是简单的FIFO策略,但是死锁的两个事务回滚事务的代价可能是不尽相同的,所以当前数据库还采用Wait-for Graph的方式来进行更积极的死锁检测,如果由锁信息链表和事务等待链表构成的图存在回路则代表存在死锁,这是一种更加主动的死锁检测机制,InnoDB通常会选择回滚undo量最小(修改记录较小)的事务执行回滚。
  在老版本的MySQL中只能通过SHOW ENGINE INNODB STATUS才能看到,而且当发生多次死锁的时候,之前的死锁记录就会被删除,用上述命令只能访问最近一次的死锁信息。MySQL5.6开始,用户可以设置innodb_print_all_deadlocks变量,就可以让数据库把所有的死锁信息都记录到MySQL的错误日志文件中。

本文完!

参考