浅谈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年被批准的标准),列出来是为了引出共享锁排它锁的概念!

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

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

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

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

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

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个字节),数据结构如下:

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 页上的偏移量
数据操作
  • 查询:返回的记录需要满足两个条件。

    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),保证清除的数据对任何事务来说都是不可见的。

数据可见性

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这段的间隙,例如:{1、3、7、9、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 普通索引跟唯一索引不一样的点在于,普通索引是存在重复的可能性,
所以即使等值查询,也是按next-key加锁,
根据规则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, +∞]
根据主键修改索引字段
update user set name = 'test' where id = 1

name为非主键索引,且id为主键时,会先锁住主键id=1的记录,再获取对应的name索引的锁。
如果这时候刚好同时在执行了以下SQL:

update user set name = 'test' where name = 'me'  

假设ID为1的记录,name等于me,则以上SQL先锁住name索引,再获取对应的记录锁,会出现死锁。

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 行锁的理解及案例

你可能感兴趣的:(浅谈MySQL InnoDB锁)