InnoDB存储引擎-锁

469 阅读7分钟

什么是锁

锁存在的意义是为了支持对共享资源的并发访问,以及保证数据的一致性和完整性。

lock和latch

latch一般称为闩锁,是一种轻量级的锁,它要求锁定时间必须非常短。在InnoDB中,latch的实现有两种,分别是互斥锁和读写锁。

lock的对象是事务,用来锁定数据库中的对象,比如表,页,行。lock的对象仅在事务commit或者rollback之后释放

latch没有死锁检测机制,但是lock有。

InnoDB存储引擎中的锁

锁的类型

InnoDB实现了两种标准的行级锁

  • 共享锁(S Lock),允许多个事务同时读,事务写会阻塞。
  • 排他锁(X Lock),仅允许一个事务写,其余事务的读和写都会阻塞。

image.png

为了实现更好的加锁,InnoDB支持意向锁,什么意思呢?就是划分出更细粒度的加锁对象,组成对象组织树形式。每次给细粒度加锁,就会给粗粒度加一个同类型的意向锁。

image.png

意向锁也有两种:IX和IS

比如想要对行记录加X锁,就会给表加一个IX锁。意向锁用来实现对粗粒度加X/S锁时是否有细粒度冲突的快速判断

如果表已经有了IX锁,则说明在这个表里,至少有一行在使用X锁,所以想对这个表加X或S就是不可以的。此时就可以通过表的意向锁快速判断,而不需要遍历行锁。

所以如果某个行加X,另一行加S,即使此时会在同一个表添加IX和IS两个意向锁,但是意向锁之间不会冲突,意向锁仅仅说明,在当前粒度下,还有属于它的更小粒度加了相应的X/S锁

image.png

图示的X/S均是表锁,IS/IX均是对行加锁产生的表级意向锁,这张表阐述的是表级锁和表级意向锁之间的兼容性

一致性非锁定读

一致性非锁定读是通过MVCC来实现对某一行数据的读取不会因为这行数据的X锁而被阻塞。通过定义可以看出,这种读取方式读取的是此行的历史数据,历史数据的保存是通过快照保存的。

image.png

快照通过undo段实现,undo段用来回滚事务。同时历史数据不会被更改,所以访问快照不需要锁操作。此外,因为一个行记录可能有多个快照,所以称为MVCC(多版本并发控制)。

InnoDB对于ReadCommited以及RepeatableRead默认使用这种读取方式。但是这两个隔离界别的非锁定读的实现略有差别,前者要求每次读取最新的快照,后者要求读取事务开始时的行数据版本。

一致性锁定读

虽然一致性非锁定读通过快照实现了更好的并发,但是有时候我们需要保证数据逻辑的强一致性,此时就需要使用加锁版本的一致性锁定读。

为了在读取时加上锁,,我们需要使用加锁的Select语句。InnoDB支持两种加锁Select:

select ... for update
select ... in share mode

第一个会对读取的行加上X锁,第二个会加上S锁。在使用这两个语句时,需要手动开启事务提交。

自增长与锁

为了实现自增主键的+1操作,需要使用AUTO-INC Locking锁机制实现,这种锁是表锁,它会在完成自增长插入的SQL语句结束后自动释放,以此来提升性能,而不是事务结束。

后面引入了轻量级互斥量的自增长实现机制,提升了性能。

外键和锁

对于外键值的插入或更新,首先需要查询父表中的记录,也就是Select全表,此时会对父表加一个S锁。

锁的算法

行锁的三种算法

InnoDB有三种行锁的算法:

  • Record Lock:锁住单条记录。
  • Gap Lock:锁住一个范围,但是不包括当前记录。
  • Next-Key Lock:锁住一个范围同时包含当前记录,相当于Record Lock+Next-Key Lock。

RecordLock锁住的是索引,如果当前where后面的列没有索引,那么就会锁住主键。Next-Key Lock锁住一个范围,这样是为了解决可重复读的幻读问题,即前后读到的数据量不一致。

当Select一个范围时,如果where后面的列拥有唯一索引,那么就会使用Record Lock替代Next-Key Lock,以此来提高系统并发性。对于主键索引亦是如此;但是对于辅助索引,则会使用范围锁定,来避免有其他操作在这个范围内增删。

此外,InnoDB还会为辅助索引的下一个键值加上GapLock,其目的是为了阻止幻读问题。

在进行范围锁定时,也会把锁定的区间所包含的主键区间进行一同锁定。

现在来理一理这三个锁锁的范围:

锁定范围
Record Lockcurr_id
Gap Lock(prev_id, curr_id)
Next-Key Lock(prev_id, curr_id]

InnoDB引入范围锁是为了解决幻读,在这里再多嘴几句。

  • 为什么行锁解决不了幻读?因为行锁无法在不存在的行上加锁,插入是创造原本不存在的行。

  • 为什么还要在键后面区间加锁?因为插入操作在插入键相同时,插入在当前行后面,所以后面还要加个区间锁,前面区间理所当然需要加锁。

所以对于一个Select ... For Update操作会锁定:(prev_id, curr_id] + (curr_id, next_id)。

参考

1

2

3

对于插入操作,会判断插入键下一个值是否被锁定,如果被锁定则阻塞。

解决幻读

幻读主要是指在同一个事务的多次Select时,后面读取到了前面读取的不存在的行,也就是事务执行期间,有别的事务在Select键区间插入了新的值。

InnoDB默认隔离级别Repeatable Read支持Next-Key Lock,这样可以避免幻读。

锁问题

脏读

脏数据和脏页是不一样的概念,脏页是未刷新到磁盘的数据,脏数据是事务未提交的SQL操作。

脏读就是某个事务读取到了别的事务未提交的操作。

脏读违反了隔离性。

不可重复读

在某个事务读取某个数据集合的时间内,另一个事务对这个数据集合做出了Update操作,致使前一个事务多次读取中读取到的数据发生了变化(数据总量没变)。

不可重复读违反了一致性,因为很明显,在别的事务读数据时,有事务进行了数据更新。

丢失更新

简单来说就是事务A作出的更新还未提交时,事务B进行了另一个更新,覆盖了事务A的更新,导致事务A的更新丢失。

解决方式可以是使用串行隔离级别。

死锁

解决死锁的简单方法就是设置超时,然后回滚。但是这有一个问题,如果某个事务占用了较多的undo,或者权重大,那么超时回滚就显得不那么合理了。

所以现在的引擎大多采用等待图来判断死锁,这点和OS的实现如出一辙。简单来说就是记录每个事务需要的资源,然后使用指针,A->B表示事务A需要事务B的锁。一旦出现回路就表示存在死锁。

这是一种较为主动的死锁检测机制,每次发生死锁,回滚undo较小的事务。改进的算法由DFS->循环实现回路判断。

锁升级

如果对行加1万个锁,那还不如直接对表加锁来的快,这就是锁升级,但是InnoDB暂不支持,所以略去不表。