第六章锁

1. 什么是锁
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访[插图]。InnoDB存储引擎会在行级别上对表数据上锁,这固然不错。不过InnoDB存储引擎也会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。例如,操作缓冲池中的LRU列表,删除、添加、移动LRU列表中的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性
在数据库中,lock与latch都可以被称为“锁”。但是两者有着截然不同的含义。latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。此外,lock,正如在大多数数据库中一样,是有死锁机制的。
2. 锁的类型
InnoDB存储引擎实现了如下两种标准的行级锁:
□ 共享锁(S Lock),允许事务读一行数据。
□ 排他锁(X Lock),允许事务删除或更新一行数据。
如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容(Lock Compatible)。但若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁——这种情况称为锁不兼容。表6-3显示了共享锁和排他锁的兼容性。

InnoDB存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(IntentionLock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁,如图6-3所示。

若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。例如图6-3,如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录r加X锁之前,已经有事务对表1进行了S表锁,那么表1上已存在S锁,之后事务需要对记录r在表1上加上IX,由于不兼容,所以该事务需要等待表锁操作的完成。


InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:
1)意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
2)意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如表6-4所示。


3. 一致性非锁定读
一致性的非锁定读(consistent nonlockingread)是指InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。

所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。可以看到,非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。就图6-4所显示的,一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(MultiVersion Concurrency Control,MVCC)。

在事务隔离级别READ COMMITTED和REPEATABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。【在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本】



在会话B提交事务后,这时在会话A中再运行SELECT * FROM parent WHEREid=1的SQL语句,在READ COMMITTED和REPEATABLE事务隔离级别下得到结果就不一样了。对于READ COMMITTED的事务隔离级别,它总是读取行的最新版本,如果行被锁定了,则读取该行版本的最新一个快照(fresh snapshot)。在上述例子中,因为会话B已经提交了事务,所以查询不出来数据,而在REPEATABLE使用的还是原来的数据。
对于READ COMMITTED的事务隔离级别而言,从数据库理论的角度来看,其违反了事务ACID中的I的特性,即隔离性

4. 一致性锁定读

但是在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对于SELECT的只读操作。
InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读(locking read)操作:
    □ SELECT…FOR UPDATE
    □ SELECT…LOCK IN SHARE MODE
SELECT…FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。SELECT…LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被阻塞。对于一致性非锁定读,即使读取的行已被执行了SELECT…FOR UPDATE,也是可以进行读取的,这和之前讨论的情况一样。此外,SELECT…FOR UPDATE,SELECT…LOCK IN SHARE MODE必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句SELECT锁定语句时,务必加上BEGIN,STARTTRANSACTION或者SET AUTOCOMMIT=0。

5.  自增长与锁
自增长在数据库中是非常常见的一种属性,也是很多DBA或开发人员首选的主键方式。在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter)。当对含有自增长的计数器的表进行插入操作时,这个计数器会被初始化。

6.行锁的3种算法
InnoDB存储引擎有3种行锁的算法,其分别是:
□ Record Lock:单个行记录上的锁
□ Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
□ Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
Next-Key Lock是结合了Gap Lock和RecordLock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:

采用Next-Key Lock的锁定技术称为Next-KeyLocking。其设计的目的是为了解决PhantomProblem,Gap Lock的作用是为了阻止多个事务将记录插入到同一范围内,而这会导致Phantom Problem问题的产生。
当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。


【五大约束:
1、主键约束(Primay Key Coustraint) 唯一性,非空性
2、唯一约束 (Unique Counstraint)唯一性,可以空,但只能有一个
3、检查约束 (Check Counstraint)对该列数据的范围、格式的限制(如:年龄、性别等)
4、默认约束 (Default Counstraint)该数据的默认值
5、外键约束 (Foreign Key Counstraint)需要建立两表间的关系并引用主表的列

主键是一种约束,唯一索引是一种索引;
②一张表只能有一个主键,但可以创建多个唯一索引;
③.主键创建后一定包含一个唯一索引,唯一索引并一定是主键;
④.主键不能为null,唯一索引可以为null;
⑤.主键可以做为外键,唯一索引不行;

二 主键约束比唯一索引约束严格,当没有设定主键时,非空唯一索引自动称为主键。对于主键和唯一索引的一些区别主要如下:
1.主键不允许空值,唯一索引允许空值
2.主键只允许一个,唯一索引允许多个
3.主键产生唯一的聚集索引,唯一索引产生唯一的非聚集索引

注:聚集索引确定表中数据的物理顺序,所以是主键是唯一的(聚集就是整理数据的意思)】
Next-Key Lock降级为Record Lock仅在查询的列是唯一索引的情况下。若是辅助索引,则情况会完全不同。

7. 解决Phantom Problem
InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem(幻像问题)

InnoDB存储引擎采用Next-Key Locking的算法避免Phantom Problem。对于上述的SQL语句SELECT * FROM t WHERE a>2 FOR UPDATE,其锁住的不是5这单个值,而是对(2,+∞)这个范围加了X锁。因此任何对于这个范围的插入都是不被允许的,从而避免Phantom Problem。
InnoDB存储引擎默认的事务隔离级别是REPEATABLE READ,在该隔离级别下,其采用Next-Key Locking的方式来加锁

8.锁带来的问题
8.1脏读
    脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。而所谓脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性
脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据.

8.2不可重复读
不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了一些DML操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读。
不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。 
8.3 丢失更新
丢失更新是另一个锁导致的问题,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。例如:1)事务T1将行记录r更新为v1,但是事务T1并未提交。2)与此同时,事务T2将行记录r更新为v2,事务T2未提交。3)事务T1提交。4)事务T2提交。
在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题

8.4 阻塞
在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行。