浅谈MySQL InnoDB锁

基于MySQL 5.6.16

SQL92标准-事务级别:

  • 序列化:排它锁

53977698.png

  • 可重复读:读写锁,读读并行,写排他;因为读锁和写锁都是记录数,没法锁定不存在的记录,因此没法阻止插入,会出现幻读。

54226896.png

  • 读已提交:读写锁,读读并行,读写并行(写读不能并行);事务1读的时候,事务2能够写,事务2提交事务释放锁以后,事务1再读,就会出现不可重复。

54347871.png

session1 session2
begin tx
select name from user where id = 7 -
- begin tx
- update user set name = 'chen' where id = 7
- commit tx
select name from user where id = 7 // 数据不一致 -
commit tx -
  • 读未提交:写锁,读读并行,读写并行,写读并行;事务1读的时候,事务2能够写,事务2还未提交事务,事务1还能够再读,就会出现脏读。

54687993.png

session1 session2
begin tx -
select name from user where id = 7 -
- begin tx
- update user set name = 'chen' where id = 7
select name from user where id = 7 // 脏数据 -
commit tx -
- commit tx

以上为已过期的处理事务的方式(92年被批准的标准),列出来是为了引出共享锁排它锁的概念!html

锁:共享锁、排它锁、意向共享锁、意向排它锁

select * from user where name = 'lin' for update

以上SQL是否有加锁,对那些记录加锁?mysql

  • 当name为主键时,锁的是聚簇索引对应的记录。
  • 当name为惟一索引时,锁的是惟一索引对应的记录、聚簇索引对应的记录。
  • 当name为普通索引时,锁的是普通索引对应的记录和间隙聚簇索引对应的记录。
  • 当name不是索引时,锁的是整个表(每一条记录上加记录锁和间隙锁,实际上MySQL有作了优化,再加完锁只有会排查并释放掉不符合条件的记录上的锁,后面会讲到)。
意向锁是什么?
  • 当name不是索引时,须要锁住全部的记录,那是否是要一条条记录检查是否有加锁?这样判断的效率很是低。
  • 意向锁是表级别的,当对记录加锁时,同时会在表上加上对应的意向锁(共享锁 -> 意向共享锁,排它锁 -> 意向排它锁)。
  • 以上SQL,在加排他锁时,发现表上若是已经有了意向锁,就会被阻塞。
- 共享锁(S) 排它锁(X) 意向共享锁(IS) 意向排它锁(IX)
共享锁(S) 兼容 不兼容 兼容 不兼容
排它锁(X) 不兼容 不兼容 不兼容 不兼容
意向共享锁(IS) 兼容 不兼容 兼容 兼容
意向排它锁(IX) 不兼容 不兼容 兼容 兼容

还有其余的“自增锁”、“insert时候的隐式锁”,本文不会说明,详细可参考:解决死锁之路 - 常见 SQL 语句的加锁分析git

MVCC(Multi-Version Concurrent Control)

select * from user where name = 'lin'

以上是否有加锁?
读已提交可重复读级别下,查询使用了MVCC方式,是不加锁的。
InnoDB 每一个汇集索引都有 4 个隐藏字段,分别是行ID(DB_ROW_ID,隐含的自增ID,若是数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引),最近更改的事务ID(DB_TRX_ID,每条记录有独立的事务ID,数据修改并提交成功的同时,会将事务ID修改为当前事务ID,新ID确定会大于旧ID),undo Log 的指针(回滚指针DB_ROLL_PTR,记录删除的时候全局事务ID号,指向这条记录在undo log上的回滚数据),删除标记(记录头信息有专门的bit标志,用来表示当前记录是否已经被删除,当删除时,不会当即删除,而是打标记,而后异步删除)。
数据库每次对数据进行更新操做时,会将修改前的数据保存到undo log中,经过undo log能够实现事务回滚,而且能够根据undo log回溯到某个特定的版本的数据,实现MVCCundo log分为insertupdatedeleteupdate操做被归成一类。
70810500.png
其中DB_ROLL_PTR长度为7个字节(56个字节),数据结构以下:github

UNIV_INLINE
void
trx_undo_decode_roll_ptr(
/*=====================*/
    roll_ptr_t roll_ptr, /*!< in: roll pointer */
    ibool* is_insert, /*!< out: TRUE if insert undo log */
    ulint* rseg_id, /*!< out: rollback segment id */
    ulint* page_no, /*!< out: page number */
    ulint* offset) /*!< out: offset of the undo
                    entry within page */
{
#if DATA_ROLL_PTR_LEN != 7
# error "DATA_ROLL_PTR_LEN != 7"
#endif
#if TRUE != 1
# error "TRUE != 1"
#endif
    ut_ad(roll_ptr < (1ULL << 56));
    *offset = (ulint) roll_ptr & 0xFFFF; //获取低16位 为OFFSET
    roll_ptr >>= 16; //右移16位
    *page_no = (ulint) roll_ptr & 0xFFFFFFFF;//获取32位为 page no
    roll_ptr >>= 32;//右移32位
    *rseg_id = (ulint) roll_ptr & 0x7F;//获取7位为segment id
    roll_ptr >>= 7;//右移7位
    *is_insert = (ibool) roll_ptr; // TRUE==1 ,最高位,标识修改或插入
}
字节位置 字节长度 做用
55 1 操做类型:1=INSERT,0=UPDATE
48-54 7 undolog segment id
16-47 32 undolog 页编号
0-15 16 undolog 页上的偏移量
数据操做
  • 查询:返回的记录须要知足两个条件。sql

    1. 当前数据未被删除,或者删除事务ID大于当前事务ID。
    2. 事务ID小于当前事务ID(下面会讲到可见性)。
  • 插入:该操做生产的undo log仅仅用于事务回滚,一旦事务提交,就会被删除。
  • 删除:将旧版本信息记录在undo log中,将数据标志为删除(Deleted bit设置为1,而不是直接删除记录),设置事务ID为当前事务ID。
  • 修改:分为两种状况,update的列是不是主键列。数据库

    1. 若是不是主键列,将旧版本信息记录在undo log中,设置当前记录的事务ID为当前事务ID。
    2. 若是是主键列,update分两部执行:安全

      1. 将数据标志为删除(Deleted bit设置为1),设置事务ID为当前事务ID。
      2. 插入新的记录,新记录的事务ID为当前事务ID。
      3. 若是有二级索引,二级索引也须要作相应的更新(二级索引中包含主键项)。
undo log清理

mysql有后台purge进程来删除无用的undo log,按顺序从老到新定时扫描undo log,直到彻底清除或者遇到一个不能清除的undo log。purge进程有本身的read view(等同于进程开始时最老的活动事务以前的view,trx_sys->mvcc->clone_oldest_view),保证清除的数据对任何事务来讲都是不可见的。session

数据可见性

MySQL在RC级别下经过MVCC解决了脏读,在RR级别下经过MVCC方案解决了脏读、不可重复读。数据结构

  • RR是事务级快照、RC是语句级快照。
  • RR:在一个事务内同一快照读执行任意次数,获得的数据一致;且只能读到第一次执行前已经提交的数据或本事务内更改的数据。并发

    • 在InnoDB中,建立一个新事务的时候,InnoDB会将当前系统中的活跃事务列表(trx_sys->trx_list)建立一个副本(read view),副本中保存的是系统当前不该该被本事务看到的其余事务id列表。当用户在这个事务中要读取该行记录的时候,InnoDB会将该行当前的版本号与该read view进行比较。
    • 设该行的当前事务id为trx_id,read view中最先的事务id为trx_id_min, 最迟的事务id为trx_id_max(trx_id_max是当前全部已提交的事务中最大XID+1)。

      1. 若是trx_id < trx_id_min的话,那么代表该行记录所在的事务已经在本次新事务建立以前就提交了,因此该行记录的当前值是可见的。
      2. 若是trx_id > trx_id_max的话,那么代表该行记录所在的事务在本次新事务建立以后才开启,因此该行记录的当前值不可见
      3. 若是trx_id_min <= trx_id <= trx_id_max,遍历read view,查找trx_id是否在read view列表中:

        • 若是trx_idread view列表中,此记录的最后一次修改在read_view建立时还没有commit不可见
        • 若是trx_id不在read view列表中,此记录在read_view建立以前已经commit可见
      4. 若是该行数据不可见,则从该行记录的回滚指针DB_ROLL_PTR指向的Undo Log中取出对应的数据,而后从新从第一步开始判断
      5. 须要注意的是,新建事务(当前事务)与正在内存中commit 的事务不在活跃事务链表中。
  • RC:每次快照读均会建立新的read view

读数据时,要不要开启“读事务”

  • 若是你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
  • 若是你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证总体的读一致性,不然,在前条SQL查询以后,后条SQL查询以前,数据被其余用户改变,则该次总体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
间隙锁

RR隔离级别下,MySQL经过MVCC + next-key(记录锁 + 间隙锁(gap锁))解决了幻读,gap只跟insert冲突,gap之间不冲突

  • 快照读:简单的select操做(select * from user where name = 'lin'),属于快照读,不加锁。
  • 当前读:全部的锁定读都是当前读,也就是读取当前记录的最新版本,不会利用 undo log 读取镜像。

    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert into table values (…);
    • update table set ? where ?;
    • delete from table where ?;
    • 在Serializable级别下,全部的读都是当前读。
select * from user where age = 10 for update;

当age为普通索引时,age索引加锁以下:

1 5 10 12 15

“10”上会加上排它锁,(5, 10) 和 (10, 12)间会加上间隙锁。

RR模式是否解决了幻读?这一点还存在争议,好比github上的这一个争议:https://github.com/Yhzhtk/not...

session1 session2
begin tx
select * from user where id = 7 -
- begin tx
- inset into user(id) values(7)
- commit tx
select * from user where id = 7 for update -
commit tx -

session1的两次select查询结果不一致。
对于这个争议,要看对幻读的定义,“快照读当前读的结果不一致”属不属于幻读的范围。官网定义的“The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. ”,在我看来“same query”应该是表示彻底同样的sql,因此要么都是当前读,要么都是快照读,若是按这个理解来看,RR级别下就解决了幻读。

简易例子

摘自MySQL 加锁处理分析

delete from t1 where id = 10;

这个SQL会加什么锁?
回答这个问题,咱们须要知道如下前提:

  1. 当前事务隔离级别是什么?
  2. id是主键?惟一索引?普通索引?非索引?
  3. 是否存在id值为10的数据?
1. id是主键 + RC:

0.9010662300042598.png
结论:id是主键时,此SQL只须要在id=10这条记录上加X锁便可。

2. id是惟一索引 + RC:

0.6305007944652574.png
结论:若id列是unique列,其上有unique索引。那么SQL须要加两个X锁,一个对应于id unique索引上的id = 10的记录,另外一把锁对应于聚簇索引上的[name=’d’,id=10]的记录。

3. id是普通索引 + RC:

0.6218706292642482.png
结论:若id列上有非惟一索引,那么对应的全部知足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。

4. id是非索引字段 + RC:

0.9744838857543574.png
因为id列上没有索引,所以只能走聚簇索引,进行所有扫描。从图中能够看到,知足删除条件的记录有两条,可是,聚簇索引上全部的记录,都被加上了X锁。不管记录是否知足条件,所有被加上X锁。既不是加表锁,也不是在知足条件的记录上加记录锁。
有人可能会问?为何不是只在知足条件的记录上加锁呢?这是因为MySQL的实现决定的。若是一个条件没法经过索引快速过滤,那么存储引擎层面就会将全部记录加锁后返回,而后由MySQL Server层进行过滤。所以也就把全部的记录,都锁上了。
注:在实际的实现中,MySQL有一些改进(semi-consistent read),在MySQL Server过滤条件,发现不知足后,会调用unlock_row方法,把不知足条件的记录放锁 (违背了2PL的约束)。这样作,保证了最后只会持有知足条件记录上的锁,可是每条记录的加锁操做仍是不能省略的。
结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,因为过滤是由MySQL Server层面进行的。所以每条记录,不管是否知足条件,都会被加上X锁。可是,为了效率考量,MySQL作了优化,对于不知足条件的记录,会在判断后放锁,最终持有的,是知足条件的记录上的锁,可是不知足条件的记录上的加锁/放锁动做不会省略。同时,优化也违背了2PL的约束。

5. id是主键 + RR:与第1个同样。
6. id是惟一索引 + RR:与第2个同样。
7. id是普通索引 + RR:

0.5139588195803471.png

  • 与第3个区别在于,这多了一个间隙锁(GAP锁),并且GAP锁不是加在记录上的,而是加载两条记录之间的位置。
  • Insert操做,如insert [10,aa],首先会定位到[6,c]与[10,b]间,而后在插入前,会检查这个GAP是否已经被锁上,若是被锁上,则Insert不能插入记录。

结论:Repeatable Read隔离级别下,id列上有一个非惟一索引,对应SQL:delete from t1 where id = 10; 首先,经过id索引定位到第一条知足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,而后加主键聚簇索引上的记录X锁,而后返回;而后读取下一条,重复进行。直至进行到第一条不知足条件的记录[11,f],此时,不须要加记录X锁,可是仍旧须要加GAP锁,最后返回结束。

8. id是非索引字段 + RR:

0.2613144302545063.png
如图,这是一个很恐怖的现象。首先,聚簇索引上的全部记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有6条记录,一共须要6个记录锁,7个GAP锁。试想,若是表上有1000万条记录呢?
在这种状况下,这个表上,除了不加锁的快照度,其余任何加锁的并发SQL,均不能执行,不能更新,不能删除,不能插入,全表被锁死。
结论:在Repeatable Read隔离级别下,若是进行全表扫描的当前读,那么会锁上表中的全部记录,同时会锁上聚簇索引内的全部GAP,杜绝全部的并发 更新/删除/插入 操做。固然,跟组合四:[id无索引, Read Committed]相似,这个状况下,也能够开启semi-consistent read,来缓解加锁开销与并发影响,可是semi-consistent read自己也会带来其余问题,不建议使用。
注:(我在:MySQL版本号5.6.16,RR级别,表数据量为223条记录的状况下作测试“update t set a = 'b'”,其中a是非索引字段,结果为:在事务结束前,会锁住全部的记录,能够经过“select * from information_schema.innodb_locks”看到,lock_model为“X”,lock_type为“RECORD”)。
注:就是所谓的semi-consistent read。semi-consistent read开启的状况下,对于不知足查询条件的记录,MySQL会提早放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]以外,全部的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了 innodb_locks_unsafe_for_binlog 参数。详细见:MySQL+InnoDB semi-consitent read原理及实现分析

9. id是普通索引 + RR + 没有id=5这条记录:

结果:会锁住小于5到大于5这段的间隙,例如:{一、三、七、九、10}数据中会锁住(3, 7)这段间隙。

一条相对复杂的SQL:

0.2508793326529062.png
结论:在Repeatable Read隔离级别下,针对一个复杂的SQL,首先须要提取其where条件。Index Key肯定的范围,须要加上GAP锁;Index Filter过滤条件,视MySQL版本是否支持ICP,若支持ICP,则不知足Index Filter的记录,不加X锁,不然须要X锁;Table Filter过滤条件,不管是否知足,都须要加X锁。
注:一个SQL中的where条件如何拆分?具体的介绍,建议阅读SQL中的where条件,在数据库中提取与应用浅析

  1. Index key:pubtime > 1 and puptime < 20。此条件,用于肯定SQL在idx_t1_pu索引上的查询范围。Index key:pubtime > 1 and puptime < 20。此条件,用于肯定SQL在idx_t1_pu索引上的查询范围。
  2. Index Filter:userid = ‘hdc’ 。此条件,能够在idx_t1_pu索引上进行过滤,但不属于Index Key。Index Filter:userid = ‘hdc’ 。此条件,能够在idx_t1_pu索引上进行过滤,但不属于Index Key。
  3. Table Filter:comment is not NULL。此条件,在idx_t1_pu索引上没法过滤,只能在聚簇索引上过滤。
  4. 从图中能够看出,在Repeatable Read隔离级别下,由Index Key所肯定的范围,被加上了GAP锁;Index Filter锁给定的条件 (userid = ‘hdc’)什么时候过滤,视MySQL的版本而定:

    • 在MySQL 5.6版本以前,不支持Index Condition Pushdown(ICP),所以Index Filter在MySQL Server层过滤,即全部符合pubtime > 1 and pubtime < 20的数据都经过回表查出来以后,才作userid = ’hdc‘过滤。
    • 在5.6后支持了Index Condition Pushdown,则在index上过滤。
    • 若不支持ICP,不知足Index Filter的记录,也须要加上记录X锁,若支持ICP,则不知足Index Filter的记录,无需加记录X锁 (图中,用红色箭头标出的X锁,是否要加,视是否支持ICP而定);而Table Filter对应的过滤条件,则在聚簇索引中读取后,在MySQL Server层面过滤,所以聚簇索引上也须要X锁。最后,选取出了一条知足条件的记录[8,hdc,d,5,good],可是加锁的数量,要远远大于知足条件的记录数量。

进阶

MySQL加锁规则里面,包含了五个原则。

  1. 加锁的基本单位是next-key lock,next-key lock 是前开后闭区间。

    select * from t where c > 12 and c < 16 for update;
    c索引时,假设 c的值有“1,3,11,15,17,20”,则加的锁为:(11, 15],(15, 17],即在(11,15)和(15, 17)上加间隙锁,15和17上加记录锁。
  2. 访问到的对象才会加锁。

    select id from t where c = 15 lock in share mode;
    c为索引,id为主键,加读锁时, 覆盖索引优化状况下, 不会访问主键索引, 所以若是要经过 lock in share mode 给行加锁避免数据被修改, 那就须要绕过索引优化, 如 select 一个不在索引中的值。
    但若是改为 for update , 则 mysql 认为接下来会更新数据, 所以会将对应主键索引也一块儿锁了。
  3. 索引上的等值查询,给惟一索引加锁的时候,next-key lock 退化为记录锁。

    当c是惟一索引或主键,假设 c的值有“1,3,11,15,17,20”。
    select * from t where c = 15 for update;
    只会在15上加记录锁。
    select * from t where c >= 15 and c < 17 for update;
    虽然查出来的结果跟 =15是同样的,可是加的锁却不同。Mysql定位到第一个符合条件的数据 15查询第一个符合条件的数据是,经过树搜索的方式定位记录,用的是“等值查询”的方法),因为在(11, 15]上是等值查询,因此退化成记录锁,总体加锁是:记录锁15和next-key (15, 17]
  4. 索引上的等值查询,向右遍历时且最后一个值不知足等值条件的时候,next-key lock 退化为间隙锁。

    当c是普通索引,假设 c的值有“1,3,11,15,17,20”。
    select * from t where c = 11 for update;
    引擎在找到 c=11这一条索引项后继续向右遍历到 c=15这一条, 此时锁范围是 (3, 11], (11, 15)
  5. 范围查询会访问到不知足条件的第一个值为止。

    select * from t where c >= 11 and c <= 15;
    当c是惟一索引,假设 c的值有“1,3,11,15,17,20”。从常理上看,c是惟一索引,不会出现重复的数据,因此加的锁应该为为“11,(11, 15]”。
    可是mysql在这种场景上,处理方式跟普通索引同样,会继续向后找第一个不知足条件的记录,最终加的锁是“11,(11, 15],(15, 17]”
示例

RR级别下,列id有如下几条数据

id
5
10
15
20
25
30
id是主键
条件 说明
id = 1 因为不存在1的值,会在第一个大于1的值上加next-key,根据规则4,等值查询会退化成间隙锁 (-∞, 5)
id < 5 根据规则5,向后查询第一个不符合条件的值,在找到的值上加next-key (-∞, 5]
id = 5 根据规则3,惟一索引/主键上等值查询,退化成记录锁 5
id <= 5 根据规则五,即便id是主键,也会继续向后查找 (-∞, 5](5, 10]
id > 5 and id < 10 (5, 10]
id >= 5 and id < 10 规则三,惟一索引上等值查找,退化成记录锁 5, (5, 10]
id >= 5 and id <= 10 5, (5, 10], (10, 15]
id = 8 8记录不存在,同第一个条件 (5, 10)
id = 10 10
id > 25 and id < 30 (25, 30]
id > 25 and id <= 30 数据末尾,会对正无穷大加锁 (25, 30], (30, +∞]
id >= 30 数据末尾,会对正无穷大加锁 30, (30, +∞]
id是普通索引
条件 说明
id = 10 普通索引跟惟一索引不同的点在于,普通索引是存在重复的可能性,<br/>因此即便等值查询,也是按next-key加锁,<br/>根据规则4,继续向后查询时,退化成间隙锁 (5, 10], (10, 15)
id = 12 12记录不存在 (10, 15)
id > 10 and id <= 15 (10, 15], (15, 20]
id >= 10 and id <= 15 (5, 10], (10, 15], (15, 20]
id不是索引
条件 说明
id =15 因为id不是索引,在没有开启semi-consistent read状况下会锁住所有数据(RR级别默认不开启) (-∞, 5], (5, 10], (10, 15], (15, 20], (20, 25], (25, 30], (30, +∞]
limit

RR级别下,普通索引id有如下几条数据

id
5
10
10
10
15
30
delete from t where id = 10

锁的是(5, 10], (10, 10], (10, 10], (10, 15)

delete from t where id = 10 limit 2

锁的是(5, 10], (10, 10]。
在删除数据的时候尽可能加 limit。这样不只能够控制删除数据的条数,让操做更安全,还能够减少加锁的范围。

order by

索引搜索就是:找到第一个值,而后向左或向右遍历。

  • order by 是用最小的值来找第一个。
  • order by desc 是用最大的值来找第一个。

RR级别下

id
5
10
15
20
25
30
select * from t where id >= 15 and id <= 20 order by id desc for update

若是id是主键索引,因为order by id desc,因此从id <= 20开始等值查询第一个符合id=20条件的数据,查找到20以后,按道理根据规则3,会退化成记录锁,但测试发现,还多锁了个间隙锁,这一点目前还没在哪一个资料上找到对这个的说明,继续向左查询,找到最后符合id >= 15的数据15,根据规则5还会继续查找到第一个不符合条件的数据10,因此最终的加锁是(5, 10], (10, 15], (15, 20], (20, 25)
若是id是普通索引,以上SQL查找到20以后,根据规则4,会退化成间隙锁,继续向左查询,找到最后符合id >= 15的数据15,根据规则5还会继续查找到第一个不符合条件的数据10,因此最终的加锁是(5, 10], (10, 15], (15, 20], (20,25)

select * from t where id >= 15 and id < 22 order by id desc for update

若是id是主键索引,因为id=22不存在,找到(20, 25)这个间隙,因为MySQL定位第一个值用的是等值查找,根据规则4,会遍历到25且退化成间隙锁,因此最终的加锁是(5, 10], (10, 15], (15, 20], (20,25)

in

RR级别下,普通索引id有如下几条数据

id
5
10
10
10
15
30
select * from t where id in (5, 10, 15) for update

MySQL是先加锁id=5,在继续id=10id=15,一个个加锁上去的。

session1 session2
begin tx begin tx
select * from t where id in (5, 10, 15) for update select * from t where id in (5, 10, 15) order by id desc for update
commit tx commit tx

order by id desc致使session2是按顺序从15, 10, 5加锁,session1和session2加锁顺序相反,致使这两条SQL可能会发生死锁。

动态间隙锁

RR级别下,主键索引id有如下几条数据

id
5
10
15
20
25
30
select * from t where id  > 15 and id <= 20 for update

以上SQL的锁是(15, 20], (20, 25],若是这时候记录15被删除(delete from t where id = 15),以上SQL的锁会动态变成(10, 20], (20, 25],

其余例子

RR级别下:
数据:

idx
2
5
9
6
14
15

事务:

session1 session2
begin tx begin tx
select * from user where idx = 3 for update select * from user where idx < 3 for update
commit tx commit tx

以上两个事物互相不干扰,session1的锁范围是(2, 5),session2的锁范围是(2, 5],间隙锁只会阻塞插入操做

session1 session2
begin tx begin tx
delete from user where idx = 7 delete from user where idx = 8
insert into user(idx) values (7) insert into user(idx) values (8)
commit tx commit tx

因为idx不存在值为7和8的记录,session1和session2都持有(5, 9)间隙锁,锁只有在事务提以后的时候才会释放,此时出现两个事务互相等待对方持有的间隙锁而没法插入,出现死锁。
避免更新或者删除不存在的记录,容易致使死锁问题。

Serializable级别做用

既然RR级别下已经不会出现幻读,那为何还须要Serializable:
防止数据丢失(被覆盖):

session1 session2
begin tx
select name from user where id = 7 -
- begin tx
- update user set name = 'chen' where id = 7
- commit tx
update user set name = 'lin' where id = 7 -
commit tx -

防止出现幻觉(RR级别,id为主键):

session1 session2
begin tx
select * from user where id = 7 // 发现数据不存在 -
- begin tx
- inset into user(id) values(7)
- commit tx
inset into user(id) values(7) // 报错,主键冲突 -
select * from user where id = 7 // 仍然发现数据不存在 -
commit tx -
参考:
InnoDB多版本(MVCC)实现简要分析
MySQL 加锁处理分析
MySQL · 引擎特性 · InnoDB undo log 漫游
SQL中的where条件,在数据库中提取与应用浅析
MySQL 在 RC 隔离级别下是如何实现读不阻塞的?
查看Mysql正在执行的事务、锁、等待
数据库分析手记 —— InnoDB锁机制分析
为何我只改一行的语句,锁这么多?
关于 MySQL 中 InnoDB 行锁的理解及案例
相关文章
相关标签/搜索