MySQL的锁


一. MySQL的锁概述

1. MySQL概述

  • MySQL是一个支持插件式存储引擎的数据库系统。
  • 不同存储引擎的表现,会有较大的区别。

2. 什么是锁?为什么使用锁?

  • 在计算机中,锁是计算机协调多个进程或多线程并发访问某一共享资源的机制。(共享资源:传统的计算资源,如CPU、RAM、I/O)
  • 在数据库中,锁就是数据库为了保证数据的一致性而使数据在被并发访问变得有序所设计的一种规则。(共享资源:数据)

3. MySQL的锁分类概述

  1. 悲观锁和乐观锁
    • 悲观锁:基于一种悲观的态度类来防止一切数据冲突,它以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作。一般数据库本身的锁机制都是基于悲观锁的机制实现的.
    • 乐观锁:对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交时才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本字段然后进行版本号的对比方式实现)。具体实现方式:表中有一个版本字段,第一次读时,获取到这个字段。处理完业务逻辑开始更新的时候,再次查看该字段的值是否和第一次的一样。如果一样更新,反之拒绝。
  2. 行级锁、表级锁、页级锁
    1. 行级锁

      1. 概述
        • Mysql中锁定粒度最细的一种锁
        • 只针对当前操作的行进行加锁
        • InnoDB支持行级锁
      2. 特点
        1. 开销大,加锁慢;
        2. 会出现死锁;
        3. 锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
    2. 表级锁

      1. 概述
        • MySQL中锁定粒度最大的一种锁。
        • 对当前操作的整张表加锁
      2. 特点
        1. 开销小,加锁快;
        2. 不会出现死锁;
        3. 锁定粒度大,发出锁冲突的概率最高,并发度最低。
    3. 页级锁

      1. 概述
        • MySQL中锁定粒度介于行级锁和表级锁中间的一种锁
        • 一次锁定相邻的一组记录
      2. 特点
        1. 开销和加锁时间界于表锁和行锁之间;
        2. 会出现死锁;
        3. 锁定粒度界于表锁和行锁之间,并发度一般。
  3. 共享锁/排他锁、表共享读锁/表独占写锁
    1. 共享锁(Shared Lock,S):对行数据的读操作加锁。

    2. 排他锁(Exclusive Lock,X):对行数据的insert/delete/update操作加锁。

    3. 表共享读锁(table read lock):表级别的S锁,对应的语句分别是LOCK TABLE … READ

    4. 表独占写锁(table write lock):表级别的X锁,对应的语句分别是LOCK TABLE … WRITE

    5. 兼容性矩阵

      当前锁模式/请求锁模式读锁写锁
      读锁
      写锁

二. MySQL常用存储引擎的锁机制

1. MyISAM

  1. 采用表级锁(table-level locking),即表共享读锁和表独占写锁。
  2. MyISAM的加锁方式:在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁;在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁。这个过程并不需要用户干预,因此用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。

2. BDB

  1. 采用页面锁(page-level locking)和表级锁,默认为页面锁

3. InnoDB

  1. 概述:支持行级锁(row-level locking)和表级锁,默认为行级锁
  2. 问题:在Innodb引擎中既支持行锁也支持表锁,那么什么时候会锁住整张表,什么时候或只锁住一行呢
    • InnoDB行锁是通过给索引上的索引项加锁来实现的。
    • 只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁
  3. 行级锁如何造成的死锁
    • 概述:在MySQL中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种:
      • 如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;
      • 如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
    • 死锁现象的产生:当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。
    • InnoBD的解决:发生死锁后,InnoDB一般都可以检测到,并使一个事务释放锁回退,另一个获取锁完成事务。

三. MySQL InnoDB的锁机制

  1. 与MyISAM不同,InnoDB的两大特点:支持事务和采用行级锁。

1. InnoDB的锁模式及加锁方法

  1. InnoDB的行锁模式共三类:共享锁、排他锁、意向锁

  2. 意向锁(Intention Locks)

    1. 定义:意向锁是表级别的锁,用来标识该表上有数据被锁住(或即将被锁)。

    2. 意向锁提出的目的:为了方便检测表级锁和行级锁之间的冲突,即为了检测是否有人请求锁定表中的某一行数据

    3. 使用:在给一行记录加锁(S)前,首先给所在表加IS锁。同理,给行记录加X锁之前一定会加上IX锁。意向锁是InnoDB自动加的,不需用户干预。

    4. 意向锁的好处

      1. 如果没有意向锁:当已经有人使用行锁对表中的某一行进行修改时,如果另外一个请求要对全表进行修改,那么就需要对所有的行是否被锁定进行扫描,在这种情况下,效率是非常低的;
      2. 引入意向锁之后:当有人使用行锁对表中的某一行进行修改之前,会先为表添加意向互斥锁(IX),再为行记录添加互斥锁(X),在这时如果有人尝试对全表进行修改就不需要判断表中的每一行数据是否被加锁了,只需要通过等待意向互斥锁被释放就可以了。
    5. 意向锁的分类

      1. 意向共享锁(IS):对整个表的读操作加锁。
      2. 意向排他锁(IX):对整个表的写操作加锁。
    6. 锁之间的兼容性(+表示兼容,-表示冲突)

      \ISIXSX
      IS+++-
      IX++--
      S+-+-
      X----
  3. InnoDB的加锁方式

行锁类型加锁释放锁
共享锁1.Serializable隔离级别:普通select
2.其他隔离级别:显示使用select…lock in share mode
事务提交或回滚后会自动释放锁
排它锁RR隔离级别:select…for update/写操作(insert update delete)事务提交或回滚后会自动释放锁
意向锁在给一行记录加锁(S/X)前,首先给所在表加IS/IX锁事务提交或回滚后会自动释放锁

2. InnoDB的锁类型

  1. 行锁(Record Lock)

    • 对单条记录的索引加锁。Record lock锁住的永远是索引,而非记录本身。
    • Innodb一定存在聚簇索引,因此行锁最终都会落到聚簇索引上。
    • 即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。因此,当一条sql没有走任何索引时,那么将会在每一条聚集索引后面加X锁,这个类似于表锁。
  2. 间隙锁(Gap Lock)

    • 对索引间隙加锁,可以是在一条索引记录之前,也可以在一条索引记录之后。
    • gap lock的唯一作用:阻止其他事务向锁住的gap里插入数据。
    • gap s lock和gap x lock可以共存(兼容),其作用也是完全相同。
    • 间隙锁只针对事务隔离级别为可重复读或以上级别
  3. Next-Key Lock

    • Next-Key lock是行锁和间隙锁的组合。一个next-key lock =[对应的索引记录的record lock + 该索引前面的间隙的gap lock],因此很多资料会用左开右闭的区间来表示next-key lock,例如(1,3]。
    • 虽然说Next-Key Lock代表着record lock+前一个间隙的gap lock,但在必要的情况下,最后一条记录后面的gap也有可能作为一条单独的gap lock被锁住。
    • 三者对比(有一个索引的行有:10,11,13,20)
      2. 行锁包括:10,11,13,20
      3. gap lock包括:(负无穷小,10),(10,11),(11,13),(13,20),(20,正无穷大)
      4. next-key lock包括:(负无穷小,10],(10,11],(11,13],(13,20],(20,正无穷大]
  4. 插入意向锁(Insert Intention Lock)

    1. 提出目的:InnoDB在RR事务隔离级别下,使用插入意向锁来控制和解决并发插入的问题。
    2. 插入意向锁是一种特殊的间隙锁,执行insert之前数据库会自动向插入的间隙加插入意向锁。
    3. 含义:该锁表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。
    4. 插入意向锁与已有的间隙锁冲突,因此间隙锁锁住的间隙是不能插入数据的
    5. 插入意向锁之间互不冲突,因此允许同时向同一个间隙插入不同主键的数据
  5. 兼容矩阵(表示兼容,-表示冲突)

    要加的锁\已存在的锁record lockgap lockinsert intent locknext key lock
    record lock-++-
    gap lock++++
    insert intent lock+-+-
    next key lock-++-

3. 快照读和当前读

  1. 概述:在Mysql中InnoDB,提供了两种事务隔离技术。第一个是mvcc(RR及以上隔离级别才可以用),第二个是next-key技术。

    • mvcc的优势是不加锁,并发性高。缺点是不是实时数据。不加lock in share mode之类的快照读就使用mvcc。
    • next-key的优势是获取实时数据,但是需要加锁。
    • 在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
  2. 快照读(snapshot read)历史数据-mvcc

    • 概述
      • 即不加锁读,读取记录的快照版本而非最新版本,通过MVCC实现。
      • 在RR隔离级别下,mvcc技术不仅可以保证可重复读,而且可以防止幻读。但它防止的是快照读,也就是读取的数据虽然是一致的,但是数据是历史数据。(mysql使用mvcc利用历史数据部分避免了幻读(在某些场景看上去规避了幻读))
      • 在rr级别下,mvcc完全解决了重复读,但并不能真正的完全避免幻读,只是在部分场景下利用历史数据规避了幻读。如果要完全避免,需要手动加锁将快照读调整为当前读(mysql不会自动加锁),然后mysql使用next-key完全避免了幻读。
      • 事务的快照时间点是以第一个select来确认的。
    • 实现:简单的select操作(不包括 select … lock in share mode, select … for update)
  3. 当前读(current read)最新数据-next key

    • 概述:即加锁读,读取记录的最新版本,会加锁保证其他并发事务不能修改当前记录,直至获取锁的事务释放锁。
    • next key技术不仅可以保证可重复读,而且可以防止幻读,同时读取的数据是最新的数据。
    • 实现
      select ... lock in share mode
      select ... for update
      insert
      update
      delete
      
多版本并发控制(MVCC)
  1. 概述

    • MVCC是通过某个时间点上的数据快照对数据的版本控制,目的是为了避免使用各种锁影响并发性能
    • 根据事务开始的时间不同,同一个时刻不同事务看到的相同表里的数据可能是不同的。
    • MVCC并不是MySql独有的,Oracle,PostgreSQL等都在使用,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。
    • 可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作。
  2. 优势:MVCC实现了非阻塞的读操作,读写不冲突。对于读操作多于写操作的应用,极大的增加了系统的并发性能

  3. InnoDB存储引擎MVCC的实现原理

    • 每一行数据中额外保存两个隐藏的列:当前行创建时的版本号删除时的版本号(可能为空)。这里记录行的版本号并不是实际的时间值,而是系统版本号
  4. 版本号

    1. 系统版本号:每开始一个新的事务,系统版本号就会自动增加
    2. 事务版本号:事务开始时刻的系统版本号
  5. 事务内执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的。MVCC具体的操作如下

    1. 执行select操作要符合以下两个条件的记录才能被事务查询出来
      1. 创建版本号 小于或者等于 当前事务版本号 ,就是说记录创建是在事务中(等于的情况)或者事务启动之前。。
      2. 删除版本号大于当前事务版本号 或者 删除版本号为空,就是说删除操作是在当前事务启动之后做的,或者没删除。
    2. 插入操作新记录的创建版本号就是事务版本号
      • 例如:插入一条记录, 事务id 假设是1 ,那么记录如下:
    3. 更新操作。先标记旧的那行记录为已删除,并且删除版本号是事务版本号,然后插入一行新的记录,将事务版本号作为创建版本号
      • 例如:针对上面那行记录,事务Id为2 要把name字段更新:update table set name= 'new_value' where id=1;,则记录如下:
    4. 删除操作。把事务版本号作为删除版本号
      • 举例:delete from table where id=1;
    5. 总结
  6. 注意:只有read-committed和 repeatable-read 两种事务隔离级别才能使用MVCC

    • read-uncommited由于是读到未提交的,所以不存在版本的问题
    • serializable 则会对所有读取的行加锁。

四. InnoDB加锁分析

  1. 默认情况下,InnoDB工作在可重复读隔离级别下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。

1. select加锁分析

  • 表结构
CREATE TABLE `aTable` (
  `a` int(11) NOT NULL,
  `b` int(11) DEFAULT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`a`),
  UNIQUE KEY `idx_b` (`b`),
  KEY `idx_c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

dba:lc_3> select * from aTable;
+---+------+------+------+
| a |    b |    c |    d |
+---+------+------+------+
| 1 |    3 |    5 |    7 |
| 3 |    5 |    7 |    9 |
| 5 |    7 |    9 |   11 |
| 7 |    9 |   11 |   13 |
+---+------+------+------+
1.1 RC+条件列为非索引列
  1. 普通select:不加锁,快照读。
# 等值查询:不加锁
select * from aTable where d=11;
# 非等值查询:不加锁
select * from aTable where d>11;
  1. 非普通select:在表中所有记录的聚集索引加S|X锁,然后自动释放不满足条件的锁
# 等值查询:在a=3的聚簇索引上加S|X锁
select * from aTable where d=9 [lock in share mode|for update];
# 非等值查询:在a=5,7的聚簇索引上加S|X锁
select * from aTable where d>9 [lock in share mode|for update];
1.2 RC+条件列为聚集索引
  • 与1.1类似。
  1. 普通select:不加锁,快照读。
# 等值查询:不加锁
select * from aTable where a=1;
# 非等值查询:不加锁
select * from aTable where a>1;
  1. 非普通select:聚簇索引加行锁
# 等值查询:在a=3的聚簇索引上加S|X锁
select * from aTable where a=3 [lock in share mode|for update];
# 非等值查询:在a=5,7的聚簇索引上加S|X锁
select * from aTable where a>3 [lock in share mode|for update];
  1. 非普通select,查询结果为空:不加锁

为什么条件列加不加索引,加锁情况是一样的?
看起来结果一样,其实是不一样的。在RC隔离级别中,MySQL Server做了优化。在条件列没有索引的情况下,尽管通过聚簇索引来扫描全表,进行全表加锁。但是,MySQL Server层会进行过滤并把不符合条件的锁当即释放掉,因此你看起来最终结果是一样的。但是RC条件列非索引比本例多了一个释放不符合条件的锁的过程!

1.3 RC+条件列为非聚集索引(唯一索引或非唯一索引)
  • 总结:与1.2类似,唯一区别是这个时候有两棵索引树,加锁是加在对应的非聚簇索引树和聚簇索引树上。
  1. 普通select:不加锁,快照读。
  2. 非普通select:二级索引加行锁,主键索引加行锁
# 1. 等值查询
# 对二级索引idx_b:加record lock,[9]
# 对主键索引primary:加record lock,[7]
select * from aTable where b=9 for update;

# 2. 非等值查询
# 二级索引idx_b:加record lock,[7],[9]
# 对主键索引primary:加record lock,[5],[7]
select * from aTable where b>5 for update;
1.4 RR+条件列为非索引列
  • 总结:加锁特征为怎么查都锁全表。
  1. 普通select:不加锁,快照读。
# 等值查询:不加锁
select * from aTable where d=11;
# 非等值查询:不加锁
select * from aTable where d>11;
  1. 非普通select:全表所有记录的聚簇索引加行锁,所有间隙的聚集索引加gap锁
  2. 实际实现:与1.1类似,MySQL也做了一些优化,就是所谓的semi-consistent read。semi-consistent read开启的情况下,对于不满足查询条件的记录,MySQL会提前放锁。所有不满足条件的记录锁都会被释放,同时不加GAP锁。
  3. semi-consistent read
    1. semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了innodb_locks_unsafe_for_binlog 参数。
    2. semi-consistent read说明:触发semi-consistent read可以缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用。
# 1. 等值查询
# 共享锁:在a=1,3,5,7(全表所有记录)的聚簇索引上加S锁,并且在聚簇索引的所有间隙(-∞,1)(1,3)(3,5)(5,7)上加间隙锁
select * from aTable where d=11 lock in share mode;
# 排他锁:在a=1,3,5,7(全表所有记录)的聚簇索引上加X锁,并且在聚簇索引的所有间隙(-∞,1)(1,3)(3,5)(5,7)上加间隙锁
select * from aTable where d=11 for update;

# 2. 非等值查询
# 共享锁:在a=1,3,5,7(全表所有记录)的聚簇索引上加S锁,并且在聚簇索引的所有间隙(-∞,1)(1,3)(3,5)(5,7)上加间隙锁
select * from aTable where d>11 lock in share mode;
# 排他锁:在a=1,3,5,7(全表所有记录)的聚簇索引上加X锁,并且在聚簇索引的所有间隙(-∞,1)(1,3)(3,5)(5,7)上加间隙锁
select * from aTable where d>11 for update;
1.5 RR+条件列为聚集索引
  • 非普通select的总结
    • 等值查询:只存在行锁;
    • 非等值查询:存在next-key锁。
  1. 普通select:不加锁,快照读。
# 等值查询:不加锁
select * from aTable where a=1;
# 非等值查询:不加锁
select * from aTable where a>1;
  1. 非普通select,等值查询:只加行锁
# 共享锁:在a=1的聚集索引上加S锁
select * from aTable where a=1 lock in share mode;
# 排他锁:在a=1的聚集索引上加X锁
select * from aTable where a=1 for update;
  1. 非普通select,>:在聚集索引上next-key锁
# 在聚集索引a上加next-key lock,(3,5](5,7](7,+∞)
select * from aTable where a>3 lock [in share mode|for update];
  1. 非普通select,>=:在聚集索引上next-key锁
# 在聚集索引a上加next-key lock,(1,3](3,5](5,7](7,+∞)
select * from aTable where a>=3 lock [in share mode|for update];
  1. 非普通select,<:在聚集索引上next-key锁
# 在聚集索引a上加next-key lock,在(-∞,1](1,3)上加gap lock
select * from aTable where a<3 [lock in share mode|for update];
  1. 非普通select,<=:在聚集索引上next-key锁
# 在聚集索引a上加next-key lock,在(-∞,1](1,3](3,5]上加gap lock
select * from aTable where a<=3 [lock in share mode|for update];
  1. 非普通select,查询结果为空:加gap锁
# 等值查询:a=6是不存在的列,这种情况会在(5,7)上加gap lock
select * from aTable where a=6 [lock in share mode|for update];
# 非等值查询:a>10的查询结果为空,这种情况会在(7,+∞)上加gap lock
select * from aTable where a>10 [lock in share mode|for update];
1.6 RR+条件列为唯一索引(不包含聚集索引)
  • 总结:与1.5类似,唯一区别是这个时候有两棵索引树,加锁是加在对应的非聚簇索引树和聚簇索引树上。
  1. 普通select:不加锁,快照读。
  2. 非普通select,等值查询:二级索引加行锁,主键索引加行锁
# 对二级索引idx_b:加record lock,[9]
# 对主键索引primary:加record lock,[7]
select * from aTable where b=9 for update;
  1. 非普通select,非等值查询(>,>=,<,<=):二级索引加next-key lock,主键索引加行锁。
# 1. > 
# 二级索引idx_b:加next-key lock, (7,9],(9,∞];
# 对主键索引primary:加record lock,[7]
select * from aTable where b>7 for update;

# 2. >=
# 对二级索引idx_b:加next-key lock, (5,7],(7,9],(9,∞]
# 对主键索引primary:加record lock,[5],[7]
select * from aTable where b>=7 for update;

# 3. <
# 对二级索引idx_c:加next-key lock, (-∞,3],(3,5]
# 对主键索引primary:加record lock,[1]
select * from aTable where b<5 for update;

# 4. <=
# 对二级索引idx_c:加next-key lock, (-∞,3],(3,5](5,7]
# 对主键索引primary:加record lock,[1]
select * from aTable where b<5 for update;
  1. 非普通select,查询结果为空:加gap锁
# 等值查询:b=6是不存在的列,这种情况会在(5,7)上加gap lock
select * from aTable where b=6 [lock in share mode|for update];
# 非等值查询:b>10的查询结果为空,这种情况会在(9,+∞)上加gap lock
select * from aTable where b>10 [lock in share mode|for update];
1.7 RR+条件列为非唯一索引
  • 总结:无论是否为等值查询,都是next-key lock。
  1. 普通select:不加锁,快照读。
  2. 非普通select,等值查询:所有满足条件记录的二级索引加next-key锁,主键索引加行锁
  • 细节:首先,通过id索引定位到第一条满足查询条件的记录,加记录上的S|X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录S|X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录,此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。
# 对二级索引idx_c:加next-key lock,(7,9],(9,11]
# 对主键索引primary: 加record lock,[5]
select * from aTable where c=9 for update;
  1. 非普通select,非等值查询:二级索引加next-key锁,主键索引加行锁
# 1. >
# 对二级索引idx_c:加next-key lock, (9,11],(11,∞]
# 对主键索引primary:加record lock,[7]
select * from aTable where c>9 for update;

# 2. >=
# 对二级索引idx_c:加next-key lock, (7,9],(9,11],(11,∞]
# 对主键索引primary:加record lock,[5],[7]
select * from aTable where c>=9 for update;

# 3. <
# 对二级索引idx_c:加next-key lock, (-∞,5],(5,7]
# 对主键索引primary:加record lock,[1]
select * from aTable where c<7 for update;

2. delete加锁分析

delete from t1 where id = 10;
  1. RC+id无索引:表中所有记录的聚集索引都加X锁,然后自动释放不满足条件的锁
  2. RC+id聚集索引:聚集索引加X行锁
    • 将主键上,id = 10的记录加上X锁
  3. RC+id非聚集索引(唯一索引或非唯一索引):非聚簇索引加X行锁,主键索引加X行锁
    (1)唯一索引

    (2)非唯一索引
    • 在id列索引上,满足id=10查询条件的所有记录索引加锁,这些记录在主键索引上的记录也会被加锁。
  4. RR+id无索引:全表所有记录的聚集索引都加X锁,所有间隙的聚集索引加gap锁
  5. RR+id聚集索引
    (1)等值查询:在聚集索引上加X行锁
    (2)非等值查询:同select-1.5
  6. RR+id唯一索引
    (1)等值查询:唯一索引加X行锁,主键索引X行锁
    (2)非等值查询:同select-1.6
  7. RR+id非唯一索引:所有满足条件记录的二级索引加next-key锁,主键索引加行锁

3. update操作加锁及其执行流程

  1. update加锁同delete操作,但可能会比update更复杂,还没有细细研究。
  2. 一个update操作的执行流程(delete同理,insert稍有区别,它会触发Unique Key的冲突检查)
    • 一个Update操作的具体流程:当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。
    • 因此,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕。

4. RR+insert加锁分析

4.1 insert加锁过程总结
  1. insert N的流程(没有唯一索引的情况)
1. 获取插入意向锁:检查当前插入位置的下一条记录M上是否存在锁对象。
    如果M上存在锁对象,且该锁对象锁住了gap,则冲突等待;
    否则,插入意向锁获取成功。
2. 插入新记录N,并对记录N的所有索引项加X行锁。
  1. insert N的流程(有唯一索引的情况)
1. 找到大于N的第一条记录M,以及前一条记录P。
2. 获取插入意向锁
    如果M上面没有gap/next-key lock,则插入意向锁获取成功,跳转到步骤3;
    否则,冲突等待
3. 唯一键冲突检查:判断P是否等于N。
    如果不相等(表示唯一键不冲突),则跳转到步骤4;
    如果相等(表示唯一键冲突),则判断P是否有锁。
        如果无锁,则报duplicate key(说明该记录已经存在,报重复值错误),并申请S next-key lock(该记录以及与前一条记录之间的间隙)(说明该记录被标记为删除, 事务已经提交,还没来得及purge)。
        如果有锁,则申请S next-key lock(说明该记录被标记为删除,事务还未提交)。
            --->
        如果申请S next-key lock成功,则获取S next-key lock,并跳转到步骤4;
        否则,处于等待队列,等待获取锁。
4. 插入新记录N,并对记录N的所有索引项加X行锁。
4.2 例子
  1. 表结构
CREATE TABLE `t3` (
`c1` int(11) NOT NULL AUTO_INCREMENT,
`c2` int(11) DEFAULT NULL,
PRIMARY KEY (`c1`),
UNIQUE KEY `c2` (`c2`)
) ENGINE=InnoDB
mysql> select * from t3;
+----+------+
| c1 | c2   |
+----+------+
|  1 |    1 |
| 15 |   15 |
| 18 |   18 |
| 20 |   20 |
+----+------+
  1. 场景
session1session2-1session2-2
insert into t3(c1,c2) values(19,18)
insert into t3(c1,c2) values(22,19)
insert into t3(c1,c2) values(22,17)
  1. 场景解析
    1. session1执行完后,表加IX锁,报duplicate key,并在唯一索引c2=15上加S next-key锁,即(15,18]。
    2. 执行session2:插入成功。
    3. 执行session3:插入意向锁c2=(15,18)阻塞,等待。

五. 查找事务的加锁情况

# 1. 输出每个事务当前持有的锁结果
show engine innodb status;

# 2. MySQL旧版本
# 记录所有innodb正在等待的锁和被等待的锁
select * from information_schema.innodb_locks\G;
# 记录所有innodb锁的持有和等待关系
select * from information_schema.innodb_lock_waits\G;

# 3. MySQL 新版本
# 记录所有innodb正在等待的锁和被等待的锁
SELECT * FROM performance_schema.data_locks\G;
  1. 表述方式
    • lock_mode X locks rec but not gap:x record lock
    • lock_mode X:x next-key lock,即当前记录的record lock+前一个间隙的gap lock
    • lock_mode X locks gap before rec:对应索引记录前一个间隙的gap lock

六. 名词解释

  1. for update
    • 如果加入了for update,则一旦发现(符合查询条件的)这批数据正在被修改,则不会发出该select语句查询,直到数据被修改结束(被commit),马上自动执行这个select语句。同样,如果该查询语句发出后,有人需要修改这批数据(中的一条或几条),它也必须等到查询结束后(commit)后,才能修改。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值