InnoDB 有哪些锁?其表现和意义是什么?增删改查分别加什么锁?死锁是怎么产生的?怎么分析和避免?本文对这些问题做了些整理,没有写的很细,适合当大纲或者总结来看。
1. IoonDB锁机制
1.1 锁生存周期
2PL(Two-Phase Locking)
二阶段锁,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。
- 加锁:实际访问到某个待更新的行时,对其加锁(而非一开始就将所有的锁都一次性持有);
- 解锁:事务提交/回滚时(而非语句结束时就释放)。
1.2 锁模式
InnoDB实现标准的行级锁,其中有两种模式的锁:
- 共享锁,LOCK_S
- 排他锁,LOCK_X
通常有一种含糊的说法:对行加S锁,其他事务只能对该行读,不能修改;对行加X锁,其他事务对该行不可读、写。其实这是不准确的,且不说InnoDB中的“读”为快照读不加锁,其关系也完全颠倒混乱了。应该以这样的顺序来理解:
- 事务T1执行 select * from t1 where id=1 lock in share mode,会先获取id=1行的共享(S)锁,才允许读取id=1这一行的数据;
- 事务T2同样执行 select * from t1 where id=1 lock in share mode,也要先申请获取id=1行的S锁,因为S锁之间兼容,可以成功获取锁,得以查询数据;
- 事务T3执行 update t1 set a=99 where id=1,要先申请id=1行的X锁,因为X锁与S锁互斥,所以获取不成功,需要等待,则更新也就无法执行。
所以对某行加S锁,只能说该事务可以读取这一行;对某行加X锁,只能说该事务可以读取、修改这一行。而不能直接说不允许其他事务读或者修改这一行。
1.3 锁属性
InnoDB实现的是行级锁没错,但是一细分,其实还有其他属性的锁,下面一一介绍。
-
记录锁(Record Locks)
记录锁定是对索引记录的锁定。请记住,对象是索引上的记录。 -
间隙锁(Gap Locks)
间隙锁定是对索引记录之间的间隙的锁定。REPEATABLE-READ隔离级别就是通过间隙锁解决幻读的:T1锁定间隙,其他事务就无法在间隙中插入数据,这样T1下一次读到的数据不会变多。实际上除了REPEATABLE-READ隔离级别,在 READ-COMMITTED 隔离级别,也会存在 gap lock ,只发生在:唯一约束检查到有唯一冲突的时候,会加 S Next-key Lock,即对记录以及与和上一条记录之间的间隙加共享锁。
-
Next-Key Locks
Next-Key Locks是索引记录上的记录锁和上一条记录之间的间隙上的间隙锁的组合。 -
意向锁(Intention Locks)
意向锁是一种表锁,分两种:- 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)。事务获取某些行的 S 锁前,必须先获得表的 IS 锁。
SELECT column FROM table ... LOCK IN SHARE MODE;
- 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)。事务获取某些行的 X 锁前,先获得表的 IX 锁。
SELECT column FROM table ... FOR UPDATE;
它是为了允许行锁和 server 层表级锁并存,并且高效而存在的。在执行 lock table t read 时,想要获取表锁,必须保证:
- 当前没有其他事务持有t表的表级排他锁;
- 当前没有其他事务持有t表中任意一行的排他锁。
为了检测是否满足第二个条件,T2必须在确保t表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法。有意向锁后,只需要检测t表是否存在排他意向锁即可。
最后一定要记住意向锁的特性:
- 意向锁是为了行锁阻塞表级锁存在的;
- 意向锁不会与行级的共享/排他锁互斥;
- 意向锁不会与意向锁互斥。
- 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)。事务获取某些行的 S 锁前,必须先获得表的 IS 锁。
-
插入意向锁(Insert Intention Locks)
执行insert时,在数据插入前需要对插入的间隙加的一种间隙锁称为插入意向锁。虽然也是一种间隙锁,但是属性特殊:- 它不会阻塞其他任何锁;
- 它本身仅会被 gap lock 阻塞。
只有在它被阻塞的时候才能观察到,所以常常会被忽略。
-
自增锁(AUTO-INC Locks)
自增锁是一种特殊的表级锁,在向具有 auto_increment 字段属性的表中插入数据时,因为要获取字段的自增值而获取的锁。由 innodb_autoinc_lock_mode 参数控制加锁行为:- 0,代表传统模式,也就是说,在对有自增属性的字段插入记录时,会持续持有一个表级别的自增锁,直到语句执行结束为止。
- 1,默认值,普通 insert 能够确定行数的,可能一次性获取到需要的自增值,自增锁在申请之后马上释放。类似 insert ...select 这样不确定插入的行数时,需要等语句执行完才释放自增锁;
- 2,建议使用,自增值即取即用,可以并发获取,但是会不连续。所有 insert 类型都是申请后就释放锁(需要 binlog_format=row,才能保证主从数据一致)。
1.4 锁组合
上面详细介绍了锁模式和锁属性,这两者是可以组合的,比如:记录锁+LOCK_S、记录锁+LOCK_X
1.5 锁冲突矩阵
要分析死锁,一定要清楚锁与锁之间的冲突关系: 锁冲突矩阵1.6 SQL与加锁
有了前面的认识,接下来要掌握的就是在InnoDB中各种SQL操作需要加的锁。先记住一个概念:同一个SQL,在不同的索引、不同的隔离级别下加的锁是不同的。下面我们分别介绍在RC隔离级别下增、删、改、查4种SQL操作的加锁行为。
-
select
快照读:不加锁(普通的 select * from t where id=1);
当前读:对扫描的行记录加相应的锁(显示加锁的读 select * from t where id=1 for update,修改数据也属于当前读)。 -
delete
对满足条件的所有记录加排他锁:LOCK_X + LOCK_REC_NOT_GAP -
insert
无unique key时:LOCK_INSERT_INTENTION + LOCK_X + LOCK_REC_NOT_GAP
有unique key时:- 先进行唯一性约束检查,如果发生冲突,会加 S Next-Key Lock,否则不加;
- 如果没有冲突,接下来向插入间隙加插入意向锁 LOCK_INSERT_INTENTION,如果该间隙已经存在Gap Lock,会被阻塞;
- 如果没被Gap Lock阻塞,数据插入成功,最后加 LOCK_X + LOCK_REC_NOT_GAP
-
update
update可以看作delete+insert的组合:- Step 1:定位到下一条满足查询条件的记录(查询过程)
- Step 2:删除当前定位到的记录(标记为删除状态)
- Step 3:拼装更新后项,根据更新后项定位到新的插入位置
- Step 4:在新的插入位置,判断是否存在 Unique 冲突(存在Unique Key时)
- Step 5:插入更新后项(不存在Unique冲突时)
- Step 6:重复Step 1到Step 5的操作,直至扫描完整个查询范围
2. 阅读死锁日志
MySQL默认是开启死锁检测的,一旦发生死锁,InnoDB会回滚其中一个事务,将锁解放。默认 show engine innodb status 会记录上一次的死锁日志,也可以设置innodb_print_all_deadlocks 将每一次死锁的日志记录到error log中。
死锁日志中列出了死锁发生的时间,以及导致死锁的事务信息(只显示两个事务,如果由多个事务导致的死锁也只显示两个),并显示出每个事务正在执行的 SQL 语句(事务执行多个SQL,只会记录正在执行的那个)、等待的锁以及持有的锁信息等。死锁日志的局限性:
- 不显示事务1已经持有了什么锁;
- 不显示事务所有的SQL,也就没法推测事务1已经持有了什么锁。
3. 得到完整的事务
分析死锁原因的第2步就是联系开发获取事务1、事务2的全部SQL,然后写出每个SQL加的锁,构造可能死锁的执行顺序,然后进行复现。每个事务中SQL的执行顺序是固定的,但是2个事务并发执行,就会有多种顺序组合,并不是都会触发死锁。举个例子:
- 同样2个事务,按以下顺序执行会死锁:
- 按以下顺序执行却不会死锁:
2个事务可以有n*m种顺序组合(n、m表示事务中的SQL数量),很难一一列举出来进行分析,所以我们需要记住一些死锁的常见原因:
- 事务以相反的顺序操作相同的数据;
- 事务以不同索引的过滤条件,来操作相同的记录;
- 存在 Unique key 的表中,insert 时容易出现由于唯一性约束检查而产生的 gap lock,导致死锁概率的增加。
4. 死锁案例
- 事务以相反的顺序操作相同的数据
- 事务以不同索引的过滤条件,来操作相同的记录
- 唯一键冲突,插入相同数据
出了开发层面避免上面这些常见的死锁逻辑,数据库层面可以设置隔离级别为READ-COMMITTED,减少Gap Lock 产生的死锁。
网友评论