MySQL锁机制

1、锁机制介绍

概念

  • 锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,数据是一种供许多用户共享的资源,因此需要借助锁来解决数据并发访问的一致性、有效性。

锁的分类

  • 锁的粒度分:

    • 行锁:是MySQL中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。 InnoDB行锁是通过给索引项加锁实现的,如果没有索引,InnoDB会通过隐藏的聚簇索引来对记录加锁。
    • 表锁:是MySQL锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的MySQL引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。
    • 页锁:是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁。
  • 使用的方式分:

    • 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会相互影响。共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。语法如下:

      1
      2
      3
      4
      # 对读取到的数据行加读锁,其它事务能够能够读取加锁的的行,但是不能修改。当事务提交时锁会释放。
      select ... lock in share mode
      # 对指定的表加读锁,其它事务只可以查询此表,但是不能修改。可以使用unlock tables命令解锁或者当一个事务提交时也会自动解锁。
      lock table <表名> read
      • 假设事务A对表1加了读锁,则:
        • 事务A可以读取表1但不可以修改表1的记录;其它事务可以查询表1的记录,但对表1的修改操作都会被阻塞直至事务A释放读锁。
        • 其它事务只能对表1加读锁而不能加写锁,直至事务A释放读锁。
    • 写锁(排他锁):当前写操作没有完成前,它会阻断其他写锁和读锁。语法如下:

      1
      2
      3
      4
      # 对读取到的数据行加写锁,其它事务不能读取和修改这些记录。当事务提交时锁会释放。
      select ... for update
      # 对指定的表加写锁,其它事务不可以查询和修改此表。可以使用unlock tables命令解锁或者当一个事务提交时也会自动解锁。
      lock table <表名> write
      • 假设事务A对表1加了写锁,则:
        • 事务A可以对表1进行查询和修改操作;其它事务对表1既不能读也不能写,直至事务A释放写锁。
        • 其它事务不能对表1加任何锁,直至事务A释放写锁。

锁算法

  • 行锁算法:

    • ①Record Lock:记录锁,单个行记录上的锁。即锁住具体的索引项,当SQL执行按照唯一性索引进行数据的检索时,查询条件等值匹配且查询的数据存在,这时SQL语句加上的锁即为记录锁。

      1
      select * from table where id = 1 FOR UPDATE; # 排它锁的记录锁算法
    • ②Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。当SQL执行按照索引进行数据的范围检索时,对于查询条件范围不存在的数据会加上间隙锁,间隙锁只在隔离级别为Repeatable Read存在。

      • 间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
      • 防止间隙内有新数据被插入。
        • 防止已存在的数据,更新成间隙内的数据。
      1
      select * FROM table where id > 1 and id < 100 FOR UPDATE; #排它锁的间隙锁算法
    • ③Next-Key Lock: 是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其它事务就不能在这个间隙修改或者插入记录。

  • 表锁算法:

    • ①意向锁:是表锁的一种。意向锁分为意向共享锁意向排它锁。意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB会先获取该数据行所在在数据表的对应意向锁。 意向锁的作用是:如果另一个任务试图在该表级别上应用共享或排它锁,则受到由第一个任务控制的表级别意向锁的阻塞。第二个任务在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。

      • 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)。

        1
        2
        # 事务要获取某些行的S锁,必须先获得表的IS锁。
        select column from table ... LOCK IN SHARE MODE
      • 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)。

        1
        2
        # 事务要获取某些行的X锁,必须先获得表的IX锁。
        SELECT column FROM table ... FOR UPDATE;
      • 假如无意向锁,且有一张users表:InnoDB,Repeatable-Read:users(id PK,name):

        id name
        1 abc
        2 def
        3 hij
        • ①事务A获取了某一行的排他锁,并未提交:

          1
          select * from users WHERE id = 2 for update;
        • ②事务B想要获取users表的共享锁:

          1
          lock table users read;
          • 因为共享锁与排他锁互斥,所以事务B在视图对users表加共享锁的时候,必须保证:

            • 当前没有其他事务持有users表的排他锁。
            • 当前没有其他事务持有users表中任意一行的排他锁。

            为了检测是否满足第二个条件,事务B必须在确保users表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法,但是有了意向锁之后,情况就不一样了。

      • 假如有意向锁:

        • ①事务A获取了某一行的排他锁,并未提交:

          1
          select * from users WHERE id = 2 for update;
          • 此时users表存在两把锁:users表上的意向排他锁与id为2的数据行上的排他锁。
        • ②事务B想要获取users表的共享锁:

          1
          lock table users read;
          • 此时事务B检测事务A持有users表的意向排他锁,就可以得知事务A必然持有该表中某些数据行的排他锁,那么事务B对users表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排他锁。
      • **意向锁不会与行级的共享/排他锁互斥,即意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性(不然直接用普通的表锁就行了)。 **

        • ①事务A获取了某一行的排他锁,并未提交:

          1
          select * from users WHERE id = 2 for update;
          • 此时事务A获取了users表上的意向排他锁和id为2的数据行上的排他锁。
        • ②事务B想要获取users表的共享锁:

          1
          lock table users read;
          • 此时事务B检测事务A持有users表的意向排他锁,就可以得知事务A必然持有该表中某些数据行的排他锁,那么事务B对users表的加锁请求就会被排斥(阻塞)。
        • ③最后事务C也想获取users表中某一行的排他锁:

          1
          select * from users WHERE id = 3 for update;
          • 事务C申请users表的意向排他锁并检测到事务A持有users表的意向排他锁。因为意向锁之间并不互斥,所以事务C获取到了users表的意向排他锁。因为id为3的数据行上不存在任何排他锁,最终事务C成功获取到了该数据行上的排他锁。
      • 总结:

        • InnoDB支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
        • 意向锁之间互不排斥,但除了IS(意向共享锁)与S(表级别的共享锁)兼容外,意向锁会与表级别的共享锁/排他锁互斥。
        • IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
        • 意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求。
    • ②自增锁:是MySQL一种特殊的锁,如果表中存在自增字段,MySQL便会自动维护一个自增锁。

2、测试MyISAM锁机制

MyISAM执行引擎只支持表锁,并且有两种实现方式: 共享读锁独占写锁

  • 测试表级写锁

    • ①首先创建一个测试表employee,这里要指定存储引擎为MyISAM,并插入两条测试数据。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      mysql> CREATE TABLE IF NOT EXISTS employee (                      
      -> id INT PRIMARY KEY auto_increment,
      -> name VARCHAR(40),
      -> money INT
      -> )ENGINE MyISAM;
      Query OK, 0 rows affected (0.07 sec)

      mysql> INSERT INTO employee(name, money) VALUES('zhangsan', 1000);
      Query OK, 1 row affected (0.01 sec)

      mysql> INSERT INTO employee(name, money) VALUES('lisi', 2000);
      Query OK, 1 row affected (0.00 sec)

      mysql> select * from employee;
      +----+----------+-------+
      | id | name | money |
      +----+----------+-------+
      | 1 | zhangsan | 1000 |
      | 2 | lisi | 2000 |
      +----+----------+-------+
      2 rows in set (0.00 sec)
    • ②开启另一个Session,并在两个Session中依次进行如下操作:

      Session1 Session2
      加表级别的写锁
      进行查询、插入或更新数据,都可以执行成功
      进行查询、插入或更新数据,可以发现都会处于等待状态
      释放写锁
      写锁释放后,插入数据操作完成
    • 结论:当一个线程获取到表级写锁后,只能由该线程对表进行读写操作,别的线程必须等待该线程释放锁以后才能操作。

  • 测试表级读锁

    • 开启两个Session,并在两个Session中依次进行如下操作:

      Session1 Session2
      加表级别的读锁
      进行插入、更新数据,发现都会报错,只能查询数据
      进行插入、更新数据,程序都会进入等待状态,只能查询数据
      释放读锁
      读锁释放后,插入数据操作完成
    • 结论:当一个线程获取到表级读锁后,该线程只能读取数据不能修改数据,其它线程也只能加读锁,不能加写锁。

3、测试InnoDB锁机制

InnoDB执行引擎除了同时支持表锁和行锁,还支持间隙锁等,并且有两种实现方式: 共享读锁独占写锁

  • 测试使用非索引查询,直接就是使用的表级锁:

    • ①首先创建一个测试表user,这里要指定存储引擎为InnoDB,并插入两条测试数据。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      mysql> CREATE TABLE IF NOT EXISTS user (      
      -> id INT PRIMARY KEY auto_increment,
      -> name VARCHAR(40),
      -> money INT
      -> )ENGINE INNODB;
      Query OK, 0 rows affected (0.14 sec)

      mysql> INSERT INTO user(name, money) VALUES('zhangsan', 1000);
      Query OK, 1 row affected (0.01 sec)

      mysql> INSERT INTO user(name, money) VALUES('lisi', 2000);
      Query OK, 1 row affected (0.04 sec)

      mysql> select * from user;
      +----+----------+-------+
      | id | name | money |
      +----+----------+-------+
      | 1 | zhangsan | 1000 |
      | 2 | lisi | 2000 |
      +----+----------+-------+
      2 rows in set (0.00 sec)
    • ②开启两个Session,并在两个Session中依次进行如下操作:

      Session1 Session2
      开启事务,使用非索引字段查询,并显式地添加写锁
      执行更新语句,上面加锁的是id=1的数据行,下面更新的是id=2的数据行,会发现程序也会进入等待状态
      提交事务,此时写锁释放
      更新语句执行成功
  • 测试使用索引查询,使用的是行锁(建议使用数据量大的表进行测试,因为是否执行索引还得看MySQL的执行计划,对于一些小表的操作,可能就直接使用全表扫描):

    • 开启两个Session,并在两个Session中依次进行如下操作:

      Session1 Session2
      开启事务,使用索引字段查询,并显式地添加写锁
      可以成功更新其它数据行
  • 测试普通索引的间隙锁:

    • ①新增一个字段num,并将num添加为普通索引,修改之前的数据使得num之间的值存在间隙。

    • ②开启两个Session,并在两个Session中依次进行如下操作:

      Session1 Session2
      开启事务,并执行下面操作
      依次执行以下新增记录语句
      以num字段升序打印表中所有记录

      根据索引的有序性,而普通索引是可以出现重复值,当Session1查询的时候只出现一条数据num=3, 为了防止第二次查询的时候出现幻读,也就是出现两条或者更多num=3这样查询条件的数据,所以理论上会给(1,3]U[3,8)区间加上锁,也就是能再插入num=1或num=8的数据(两者不在范围内),但从打印结果发现Session却不可以插入num=1的记录,这是为什么呢?

    • ③为了更好理解,先把user表数据还原成原始数据。

    • ④接着还是开启两个Session,并在两个Session中依次进行如下操作:

      Session1 Session2
      开启事务,并执行下面操作
      依次执行以下新增记录语句
      执行以下更新记录语句

      可以发现添加id=6,num=8的记录被阻塞而添加id=8,num=8的记录成功,并且将id=11,num=12的记录改成id=11,num=5也被阻塞。原因如下图,可以得出结论:在普通索引跟唯一索引中,数据间隙的分析,数据行是优先根据普通索引排序,再根据唯一索引排序,所以由于添加的id=6,num=8在num=(3, 8)的区间里,所以会被阻塞;而添加的id=8,num=8则是在num=(8, 12)区间里,所以不会被阻塞; 修改的语句相当于在num=(3, 8)的区间里插入一条数据,所以也被阻塞了:

  • 测试唯一索引是否存在间隙锁:

    • ①初始化user表数据如下:

    • ②开启两个Session,并在两个Session中依次进行如下操作:

      Session1 Session2
      开启事务,并执行下面操作
      依次执行以下插入记录语句
    • 结论:由于主键是唯一索引,而且是只使用一个索引查询,并且只锁定一条记录,所以以上的例子,只会对id=5的数据加上记录锁,而不会产生间隙锁。

参考链接:

https://zhuanlan.zhihu.com/p/150101667

https://zhuanlan.zhihu.com/p/48269420