锁
锁机制用于管理对共享资源的并发访问。
lock
与latch
,都被称为锁,但含义不同。
- latch一般成为闩锁(轻量级的锁),要求锁定时间非常短,否则性能差。分为mutex(互斥量)和rwlock(读写锁)。用来保证并发线程操作临界资源的正确性,且没有死锁检测机制。
- lock的对象是事务,用于锁定表、页、行。一般lock的对象仅在事务commit或rollback后释放(不同事务隔离级别不同,释放时间不同)。有死锁机制。
. lock latch 对象 事务 线程 保护 数据库内容 内存数据结构 持续时间 整个事务过程 临界资源 模式 行锁、表锁、意向锁 读写锁、互斥锁 死锁 通过 waits-for graph
、time out
等机制进行死锁检测、处理无死锁检测、处理机制。仅通过应用程序加锁的顺序 lock leveling
保证无死锁的情况发生。存在于 Lock Manager的哈希表中 每个数据结构的对象中
类型
InnoDB存储引擎实现了如下2种行级锁:
- 共享锁(S Lock),允许事务读一行数据。
- 排他锁(X Lock),允许事务删除或更新一行数据。
排他锁和共享锁的兼容性
. | X | S |
X | x | x |
S | x | o |
多粒度
InnoDB存储引擎支持多粒度锁定,这允许事务在行级上的锁和表级上的锁同时存在。
为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持额外的锁方式,称为意向锁(Intention Lock)。
层次: 数据库 -> 表 -> 页 -> 记录
对最细粒度的对象进行上锁,就要对粗粒度的对象上锁。
对页上的记录r上X锁,那么分别要向该数据库、该表、该页上意向锁IX,最后对记录r上X锁。
IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。意向锁其实不会阻塞除全表扫描以外的任何请求。
意义
- 如果没有意向锁,则需要遍历整个表判断是否有行锁的存在,以免发生冲突。
- 如果有了意向锁,只需要判断该意向锁与即将添加的表级锁是否兼容即可。
表级别的锁的兼容性(这里的S、X也是表级别的):
. | IS | IX | S | X |
IS | o | o | o | x |
IX | o | o | x | x |
S | o | x | o | x |
X | x | x | x | x |
理解
IX与IS都标级别的锁定,代表的意思就是有别的事务已经对当前表已经加了X锁或者S锁(不管是行级别的锁还是表级别的锁),如果此时有其他事务来申请表级别的锁会失败(上述冲突兼容表),但是非表级别的X锁会申请成功。
一致性非锁定读
是指InnoDB存储引擎通过行多版本控制的方式来读取当前执行时间数据库中行的数据。
如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上的X
锁释放。相反地,InnoDB存储引擎会去读取行的一个快照。
快照是指行的之前版本的数据,该实现是通过undo段来完成。
undo用来在事务中回滚数据,因此快照数据本身没有额外开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
非锁定读机制极大地提高了数据库的并发性。InnoDB存储引擎的默认读取方式,即读取不会占用和等待表上的锁。<br/
但不同事务隔离级别下,读取的方式不同,不是都采用非锁定一致性读。
快照数据可能还有多个版本。
在事务隔离级别READ COMMITTED
和REPEATABLE READ
(默认),InnoDB存储引擎使用非锁定的一致性读。
然而,使用的快照数据的定义不同:
- 在READ COMMITTED下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。(这个事务过程中读到的是,其它事务提交的最新版本)
- 在REPEATABLE READ下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。(这个事务过程中读到的是,这个事务开始时的版本,不管其它事务的提交版本。)
READ COMMITTED
违反ACID中的I,隔离性。REPEATABLE READ
违反ACID中的C,一致性。
一致性锁定读
REPEATABLE READ
隔离级别下,InnoDB存储引擎的SELECT
操作使用一致性非锁定读。
用户可以显式地对数据库读取操作加锁,保证数据逻辑的一致性(一致性锁定读)。
InnoDB提供2种一致性锁定读(locking read):
SELECT ... FOR UPDATE
:对读取的行记录加X锁。SELECT ... LOCK IN SHARE MODE
:对读取的行记录加S锁,其它事务可以向被锁定的行加S锁,但不能加X锁,则会被阻塞。
一致性锁定读、一致性非锁定读混用的情况:对于一致性非锁定读,即使读取的行已被执行了SELECT ... FOR UPDATE
,也是可以读取的。
SELECT ... FOR UPDATE
和SELECT ... LOCK IN SHARE MODE
,必须在事务中,事务提交后,锁就释放了。 (BEGIN、START TRANSACTION、SET AUTOCOMMIT=0)
在数据库应用中的锁
自增长与锁
InnDB存储引擎的内存结构中,对每个含有自增长的表都有一个自增长计数器。每当含有自增长的计数器的表进行插入操作时,就会计数。
旧版:这个的实现就是
auto-inc locking
。这种锁采用特殊的表锁机制,为了提高插入的性能,所不是在一个事务完成后才释放,而是完成对自增长值插入的SQL语句后立即释放。(尽管不需要等事务结束就可以解锁,但大量的插入还是影响并发性能。)新版:采用轻量级互斥量的自增长实现,提高了插入的性能。
在InnoDB中,自增长值必须是索引,且为索引的第一列。否则抛出异常。
外键和锁
外键用于完整性约束检查,InnoDB会自动对外键列加索引,可以避免表锁。
对外键值的插入或更新,首先需要查询父表(select父表)。
但是对于父表的SELECT操作,不是使用一致性非锁定读的方式,这样会发生数据不一致的问题。
因此,这使用select ... lock in share mode
方式,即主动对父表加一个S锁
。(如果父表已上了X锁
,子表的操作会被阻塞。)
算法
InnoDB有3种行锁算法:
Record Lock
:单个行记录上的锁。Gap Lock
:间隙锁,锁定一个范围,但不包含记录本身。Next-Key Lock
:(Record Lock + Gap Lock)锁定一个范围,并且锁定记录本身。(还有Previous-Key Lock
)
如果建表时没有设置任何索引,那么InnoDB会使用隐式的主键来进行锁定。
InnoDB对行的查询都是采用
Next-Key Lock
锁定。
目的:是为了解决幻读问题。
优化:当查询的索引含唯一属性时,InnoDB会对Next-Key Lock
进行优化,降级为Record Lock
,即仅锁住索引本身,而不是范围。
优化(辅助索引):Next-Key Lock
降级为Record Lock
仅在查询列是唯一索引的情况下。若为辅助索引,则情况会完全不同,会锁定聚集索引
的值和辅助索引
的前后范围。(并不明确)
例如:
一个索引有10,11,13,20这4个值,可能被Next-Key Locking的区间为:
(-inf, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +inf)
Previous-Key Lock锁定的区间为:
(-inf, 10)
[10, 11)
[11, 13)
[13, 20)
[20, +inf)优化:
当查询语句为select * from t where id = 10 for update;
这样id=5为逐渐且唯一,因此锁定的仅是10这个值,而不是(-inf, 10]。其它事务插入7时不会阻塞。提高并发性。优化(辅助索引):
create table z(a int, b int, primary key(a), key(b)); insert into z select 1, 1; insert into z select 3, 1; insert into z select 5, 3; insert into z select 7, 6; insert into z select 10, 8; select * from z where b = 3 for update;
这时,通过索引列b进行查询,由于有2个索引,其需要分别进行锁定。
对于聚集索引,仅对列a为5的索引加上Record Lock
。
对于辅助索引,加上的是Next-Key Lock
,锁定范围是[1, 3)。
此外,对于辅助索引的下一个键值还会加上gap lock
,即锁定[3, 6)。
因此,其它事务执行如下语句,都会被阻塞。select * from z where a=5 lock in share mode; insert into z select 4, 2; insert into z select 6, 5;
第一个不能执行,是因为已经对聚集索引中的列a=5加上了X锁。
第二个不能执行,主键插入4,没问题,但插入辅助索引值2,在锁定范围[1, 3]中。
第三个不能执行,主键6和辅助索引5不在(1, 3]之间,但5在另一个锁定的范围(3, 6)中。
注:尝试插入(2或4或6, 6),其它取值可以。而(2或4或6, 不在1~6之间)都行。(表明现版本的InnoDB,对主键并不只是Record Lock
,而是联合2部分的锁处理。)
问题
通过锁机制可以实现事务的隔离性要求,使得事务可以并发地工作。
脏读
概念:是指未提交的数据。即一个事务读到了另一个事务中未提交的数据。
在事务隔离级别为read-uncommitted
下会发生脏读。
幻读(不可重复读,Phantom Problem)
概念:是指同一事务下,连续执行2次同样的SQL语句可能会导致不同的结果。(读到其它事务提交的数据)第二次的SQL语句可能会返回之前不存在的行。
InnoDB默认事务隔离级别REPEATABLE READ
,采用Next-Key Lock
来避免Phantom Problem
(幻像问题)。
在Next-Key Lock
算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围。
事务隔离级别
READ COMMITTED
仅采用Record Lock
。
丢失更新
概念:是指一个事务的更新被另一个事务的更新所覆盖。(即使是READ UNCOMMITTED
也不会导致丢失。)
导致数据丢失不会是数据库本身的问题,更多是逻辑上的问题。
要避免逻辑导致丢失更新发生,需要让事务变为串行操作,使用SELECT ... FOR UPDATE
加锁,避免查询的数据在事务处理期间变更。
阻塞
概念:事务的锁需要等待另一个事务的锁释放,就是阻塞。阻塞并不是坏事,是确保事务可以并发正常运行。
默认情况下,InnoDB不会回滚超时引发的错误。这是非常危险的状态。
--- 等待锁的时长 select @@innodb_lock_wait_timeout; --- 是否超时回滚(默认不回滚) select @@innodb_rollback_on_timeout;
死锁
概念:因争夺锁资源而造成的互相等待。若无外力推进,事务都将无法推进下去。
超时机制:
解决的最简单方式是都不要等待,将任何等待都转化为回滚。但会导致并发性能下降,甚至比死锁问题更严重,因为难发现并浪费资源。
另一种简单方法是超时,只回滚其中一个。
主动检测机制:使用wait-for graph
(等待图)的方式检测死锁。(InnoDB使用的方式)
wait-for graph
要求保持数据库一下两种信息:锁的信息链表、事务等待链表。
通过上述链表可以构造一张图,若在图中存在回路,就代表存在死锁。(事务为节点)
事务状态、锁信息
Transaction Wait Lists Lock Lists -- t1 row1 -- ---- t2 t2:x row2 -- ---- ---- t3 t1:s t1:s -- ---- t4 t4:s ---- t2:x ---- t3:x
- 事务t2对row1占用x锁
- 事务t1对row2占用s锁
- 事务t1需要等待事务t2中row1的资源,因此有条边从节点t1指向t2
- 事务t2需要等待事务t1、t4所占用的row2对象,所以t2->t1、t4
- 同样,t3->t1、t2、t4 wait-for graph如下
-----> t1 <----- t2 ^ / ^ \/ | /\ | / \ | v \ | t4 <----- t3
可发现存在回路(t1, t2),因此存在死锁。
在每个事务请求锁并发生等待时,都会判断是否存在回路,若存在则有死锁,通常选择回滚undo量最小的事务。
wait-for graph的死锁检测通常采用深度优先的算法实现,且为非递归。
锁升级
概念:为了降低开销,将大量行锁升级到页锁,升级到表锁。会降低并发性能。
InnoDB中不存在锁升级,由于采用的是位图方式,不管事务锁住的是页中一个记录还是多个记录,开销是一样的。