InnoDB锁

lock与latch

​ latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch分为互斥锁(mutex)和读写锁(rwlock)。用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。

​ lock的对象是事务,用来锁定的是数据库的对象,例如表、页、行。并且一般lock的对象仅在是我commit或者rollback之后进行释放。lock有死锁机制。

InnoDB存储引擎中的锁

锁的类型

行级锁

​ 共享锁(S lock):运行事务读一行数据。

​ 如果事务T1已经获取了行r的共享锁,另外的事务T2也可以立即获取到行r的共享锁。因为读数据并没有改变行r的数据,称这种情况为锁兼容(Lock Compatible)。

​ 排它锁(X lock):运行事务删除或更新一行数据。

​ 如果事务T1、T2已经获得行r的共享锁,事务T3要获取行r的排它锁,则必须等待T1、T2释放行r上的共享锁。这种情况称锁不兼容。

S锁和X锁均是行锁,X锁不能与任何的锁兼容,而S锁仅可以和S锁兼容。

意向锁

​ InnoDB支持多粒度锁定(granular),这种锁运行事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁,InnoDB存储引擎支持一种额外的锁方式,称为意向锁(Intention Lock)。

​ 意向锁将锁定的对象分为多个层次,意向锁意味着事务更希望在更细的粒度上进行加锁。

图1

​ 如上图,如果事务需要对行r上X锁,那么就需要对数据库、表、页上意向锁IX,最后对行r上X锁。如果其中任何一个部分导致等待,那么搞操作需要等待粗粒度锁的完成。例如:事务T1已经对表1进行了S表锁,那么表1上已经存在了S锁;事务T2要对表1的行r加X锁,首先要对行r所在的表1加IX锁,由于和已经存在的S锁不兼容,所以事务T2必须等待事务T1的表锁的完成,才能添加在表1上的IX锁。

在InnoDB存储引擎中,意向锁即为表级别的锁。目的是为了在一个事务中揭示下一行将被请求的锁类型,共有两种意向锁:
意向共享锁(IS Lock):事务想要获取一张表中某几行的共享锁。
意向排它锁(IX Lock):事务想要获取一张表中某几行的排它锁。

由于InnoDB支持的是行级别的锁,因此意向锁不会阻塞全表扫描以外的任务请求。

图2

自增长与锁

​ 在InnoDB存储引擎中,每个含有自增长值的表都有一个自增计数器(auto-increment counter)。当进行插入操作时,将初始化计数器,执行"select max(aotu_inc_col) from table for update;"得到计数器的值。插入操作会依据这个自增长的计数器值加1赋予自增长列。

​ 这种锁是一种表锁机制,有以下特点:

  • 为了提高插入操作的性能,锁不是在事务完成之后才释放,而是在完成自增长值的插入的sql语句后就立即释放。

  • 由于事务必须等待前一个插入语句的完成,所以并发插入性能较差。

  • 对于大量数据的插入会影响插入的性能,因为另一个事务的插入会被阻塞。

​ 从Mysql5.1.22开始,InnoDB存储引擎提供了一个参数innodb_aotuinc_lock_mode来控制自增长的模式,这种机制提高了自增长值插入的性能。

图3

参数innodb_aotuinc_lock_mode共有3个有效值可供设定,即0、1、2,默认值为1.

在InnoDB存储引擎中,自增长列必须是索引,同时必须是索引的第一个列。否则会抛出异常。

图5

图6

锁的算法

行锁的三种算法

​ Record Lock:单个行记录上的锁。

​ 记录锁是锁住记录的,但不是真实的数据记录而是索引记录。如果锁的是非主键索引,会在自己的索引上面加锁之后,然后再去主键上加锁锁住。如果表上没有索引(包括没有主键),则会使用隐藏的主键索引加锁。

​ Gap Lock:间歇锁,锁定一个范围,但不包含记录本身。

图7

​ Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。

​ 在Next-Key Lock算法下,InnoDb对于行的查询都采用这种锁定算法。例如一个索引有10,11,13,20这四个值,那么该索引可能被Next-Key Lock的区间为:

(-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)

​ 若事务T1已经通过next-key lock锁定了范围(10,11]、(11,13],当插入新记录12时,锁定的范围就变成(10,11]、(11,12]、(12,13].

​ 如果查询的索引含有唯一属性时,InnoDB会对next-key lock进行优化,降级为Record Lock,即仅锁住索引本身为不是范围。

创建测试表t:

图8

执行如下的sql。

图9

​ 表t中共有1,2,5三个值。在事务1中首先对a=5进行了X锁。而由于a是之间且唯一,因此只锁定了5这个值,而不是(2,5]这个范围,所在在事务2中插入4这个值不会阻塞,可以立即插入并返回。锁由next-key lock降级为Record Lock,提高了并发性。

如果查询的列不是唯一索引,如果是辅助锁以,如下表:

图10

表z中,列b为辅助索引,执行如下的sql。

图11

​ 在事务1中通过索引列对b进行查询,由于有两个索引,需要对其分别进行锁定。对于聚集索引,仅对列a=3的所有添加Record Lock,对于辅助索引,会加上Next-Key Lock,锁定的范围是(-1,1]。这里,InnoDB还会对辅助索引下一个键值加上gap Lock,即还有一个辅助索引范围是(1,3)的锁。

​ 因此在事务2中三条sql语句都不能执行:

​ 对于第一条sql,因为在事务1中已经对a=3添加X锁,所以阻塞。
​ 对于第二条sql,主键插入2,不会阻塞;但是插入辅助索引值0在Next-Key Lock的范围(-1,1]中,会被阻塞。
​ 对于第三天sql,主键插入4,不会阻塞,插入辅助索引2,不在Next-Key Lock的范围(-1,1]中,但是插入辅助索引在gap锁范围(1,3)中,会被阻塞。

锁问题

脏读

​ 读"脏"数据指事务T1修改某一数据,并将其写回磁盘,事务T2读取该数据后,事务T1由于某种原因没有commit数据,而是rollback。这个时候,T1修改过的数据恢复原值,T2读取到的数据就和数据库中存在的数据不一致,则T2读取到了脏数据(不正确的数据).

不可重复读

​ 不可重复读指事务T1读取数据后,事务T2执行更新操作,当事务T1无法再现前一次读取结果。情况有三种:

​ a>事务T1读取某一数据后,事务T2对其作了修改,当事务T1再此读取该数据时,得到的和前一次不同的值。
​ b>事务T1按一定条件从数据库中读取了某些数据,事务T2删除了其中的部分记录,当T1再此按相同的条件读取,发现某些记录消失了。
​ c>事务T1按一定条件从数据库中读取了某些数据,事务T2插入一些记录,当T1再此按相同的条件读取,发现多出了一些记录。

更新丢失

​ 两个事务T1和T2读取同一数据并修改,后提交事务的结果破坏了先提交事务的结果,导致了先提交事务的修改丢失。

阻塞

​ 因为不同锁之间的兼容性问题,导致在有些时刻一个事务中的锁需要等到另一个事务中的锁释放它锁占用的资源,这就是阻塞。其可以保证事务并发且正确的运行。

​ 在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来控制等待的时间(默认是50s),参数innodb_rollback_on_timeout来设定是否在等待超时的情况下对进行中的事务进行回滚(默认为OFF,不回滚)。

在默认情况下,InnoDB存储引擎不会回滚超时引发的错误异常。

死锁

​ 死锁是值两个或两个以上的事务在执行过程中,因争夺锁资源而相互等待的现象。
​ 由此可以看出,有等待才会有死锁。所以解决死锁的第一个方法就是超时。当两个事务相互等待时,其中一个超过了一定的阈值就对其进行回滚,那么另一个事务就能继续进行。

除了超时回滚的机制,当前数据库普遍采用wait-for graph(等待图)的方式来进行死锁检测。InnoDB也采用这种方式,这是一种更为主动的死锁检测方式。
wait-for graph要求数据库保存以下两种信息:

  • 锁的信息链表
  • 事务等待链表

图12

​ 上图为当前书屋和锁的状态,可以看出有4个事务分别为t1、t2、t3、t4,所以在wait-for graph图中将有4个节点。
​ 而且事务t2对row1占用x锁,事务t1对row2占用s锁。
​ 事务t1需要等待事务t2的row1的资源,因此在wait-for graph中有条边从t1指向t2.
​ 事务t2需要等待事务t1和t4的row2的资源,因此在wait-for graph中有两条边从t2分别指向t1和t4.
​ 同样可得t3有分别执行t1、t4和t2的三条边.
​ 最终wait-for graph如下图所示:
图13

通过上图发现,存在回路(t1,t2),因此存在死锁。

​ 每个事务在请求锁并发生等待的时候都会判断是否存在回路,若存在则有死锁,通常InnoDB会选择回滚undo量最小的事务(即权重小的事务)。

shui2104
原创文章 15获赞 1访问量 6602
关注私信
展开阅读全文