数据库系统使用锁是为了支持对共享资源进行并发访问,保障数据的完整性和一致性。
锁设计
不同存储引擎的锁设计是不一样的,InnoDB 存储引擎会在行级对数据上锁,不过 InnoDB 存储引擎也会在数据库内部其他多个地方使用锁。例如,操作缓存池中的LRU列表,删除、添加、移动LRU列表中的元素,为了保持一致性,必须有锁的介入。MyISAM 存储引擎中锁是表锁设计,在并发读没啥问题,但是相比行锁并发写的性能就差了很多。
InnoDB 存储引擎中的锁
InnoDB 存储引擎实现了两种标准的行级锁:
共享锁(S Lock)
:允许事务读一行数据排他锁(X Lock)
:允许事务删除或更新一行数据
如果一个事务 T1 已经获取了行 r 的共享锁,那么另外的事务 T2 也可以获得行 r 的共享锁,称锁兼容,因为读共享,并没有改变数据。但如果其他事务 T3 想获取行 r 的排他锁,则必须等待事务 T1 和 T2 释放行 r 上的共享锁,称锁不兼容。下表显示了共享锁和排他锁的兼容性:
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
InnoDB 存储引擎支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在,为了支持多粒度上加锁 InnoDB 存储引擎支持一种额外加锁方式,称之为意向锁。若将上锁的对象当作有层级的树图来看,如果想对最细粒度上锁就需要先对粗粒度上锁。如下图中,如果想对记录 r 上 X 锁,那么需要分别对数据库 A、表、页上意向 IX 锁,最后才能对记录 r 上 X 锁。
意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。意向锁的设计比较简练,其意向锁即为表级锁,目的主要为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:
意向共享锁(IS Lock)
:事务想要获得一张表中的某些行数据的共享锁意向排他锁(IX Lock)
:事务想要获得一张表中的某些行数据的排他锁 由于 InnoDB 存储引擎支持的是行级的锁,因此意向锁并不会阻塞除全部扫以外的任何请求。表级意向锁和行级锁的兼容性如下表:
IS | IX | S | X | |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
向一个表添加表级 X 锁的时候(执行 ALTER TABLE, DROP TABLE, LOCK TABLES 等操作),如果没有意向锁的话,则需要遍历所有整个表判断是否有行锁的存在,以免发生冲突。如果有了意向锁,只需要判断该意向锁与即将添加的表级锁是否兼容即可。因为意向锁的存在代表了,有行级锁的存在或者即将有行级锁的存在,因而无需遍历整个表,即可获取结果。
锁状态
在 InnoDB 1.0 版本之前,用户可以通过 SHOW ENGINE INNODB STATUS
命令查看当前锁请求的信息。从 InnoDB 1.0 版本开始,在 INFORMATION_SCHEMA 架构下添加了表 INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS 三张表,用户可以更简单地监控当前事务和锁状态。表结构如下:
锁的算法
InnoDB 存储引擎由三种行锁算法,如下:
Record Lock
:单行记录锁Gap Lock
:间隙锁,锁定一定范围,不包含记录本身Next-Key Lock
:锁定一定范围并且包含记录本身
Next-Key Lock 是结合 Record Lock 和 Gap Lock 的一种锁算法,在 Next-Key Lock 算法下,InnoDB 存储引擎对于行的查询都采用的是这种锁定方式。例如一个索引有 10、11、13 和 20 这四个值,那么该索引可能被 Next-Key Locking 的区间为:(-∞,10]、(10,11]、(11,13]、(13,20]、(20,+∞)。当查询的索引是唯一键(单列唯一),InnoDB 存储引擎会对 Next-Key Lock 进行优化降级为 Record Lock,仅需要锁住本身即可。
Next-Key Lock 锁算法解决了幻读问题。幻读问题是指在同一个事务下,连续执行两次同样的 SQL 语句可能返回不同的结果,第二次的 SQL语句可能返回之前不存在的行。通过 Next-Key Lock 算法,即使用户通过索引查询一个不存在的值,其锁定的也是一个范围,另一个事务的操作是不会执行的,这样便避免了幻读。
阻塞
锁等待就带来了阻塞,在 InnoDB 存储引擎中参数 innodb_lock_wait_timeout
用来控制等待时间,参数 innodb_rollback_on_timeout
用来设置等待超时事务是否回滚。
死锁
死锁是指两个或两个以上的事务在执行过程中,因资源竞争而造成的互相等待的现象。
死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用。
占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
若无外力作用事务将无法推进,解决死锁问题最简单的方法就是超时,即当两个事务死锁,当一个事务达到等待阈值回滚,另一个事务继续执行。超时机制简单,但如果超时的事务很大很重,回滚的代价会很大。除了超时机制,InnoDB 存储引擎还提供了 wait-for graph(等待图) 的方式进行死锁检测。
wait-for graph 要求数据库保存锁的信息链路和事务等待链路,通过它们构造出一张图,而这张图中若存在回路就代表死锁,因为资源间发生了相互等待。wait-for graph 是主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,如果存在死锁 InnoDB 存储引擎通常选择回滚 undo 量最小的事务,从而减少性能损耗。
锁升级
锁升级是指粗化当前锁的粒度,如把 1000 个行锁升级为一个页锁。InnoDB 存储引擎中不存在锁升级,这是因为 InnoDB 存储引擎不是通过行记录产生行锁的,而是通过每个事务访问的每个页通过位图对锁进行管理,因此一个事务不管是锁住一个页的一条记录还是多条记录,开销通常是一致的。
公众号文章同步github。
本文地址:github.com/lazecoding/Note/blob/main/note/articles/mysql/锁.MD