https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
解决因资源共享而造成的并发问题

1,锁的分类

1,根据操作类型分类

1,读锁(共享锁)

对同一个数据读取时,多个线程可以同时读,但是不能写,不影响数据的真实性

2,写锁(互斥锁)

对一个数据修改时,对其他的线程进行排斥,因为可能会造成数据的不真实性,所以执行写操作时,排斥其他的读操作和写操作

2,根据操作范围分类

1,表锁(对整表加锁),MyISAM 默认使用

1,MyISAM 表级锁的锁模式:

  • 执行 SELECT 语句:会对涉及到的表加读锁
  • 执行增删改语句:会对涉及到的表加写锁

所以对 MyISAM 表进行操作,会有如下两种情况:

  • 对一个表进行读操作(加读锁),不会阻塞其他会话对该表的读操作,但是会阻塞其他绘画对该表的写操作,当读锁释放后,其他会话的写操作才会继续执行
  • 对一个表进行写操作(加写锁),会阻塞其他会话对该表的读操作和写操作,当写锁释放后,其他会话的读操作和写操作才会继续执行

特点

  • 开销小:对整个表只需要加一个锁即可
  • 速度快:无需精准定位
  • 无死锁:但是因为加锁范围大,容易产生阻塞,并发度低

    2,行锁(对一行数据加锁),InnoDB 使用

    特点

  • 开销大:需要对表里每个行加锁

  • 速度慢:需要精准定位锁的位置
  • 容易产生死锁:容易产生 脏读,幻读,不可重复读,但是并发度高

    3,页锁()

    4,间隙锁(对范围数据加锁)

    间隙锁是在可重复读的隔离级别下为了解决幻读加入的锁机制。
    间隙锁(Gap Lock)是对索引记录之间的间隙进行加锁,或者对一定范围内加锁
    间隙锁的间隙区间是左开右闭
    假设某非唯一索引字段 c 有 5,10,15,20,25 五条记录

  • 等值查询时,对间隙加锁

    • where c = 6:对 (5,10] 的记录加锁
    • where c = 5:对 (-∞,5] 的记录加锁
    • where c = 26:对 (25,-∞)的记录加锁
    • where c > 16:对 (15,-∞) 的记录加锁

注意:对于唯一索引来说,间隙锁的范围是左开右开的(PS:因为唯一索引不允许重复)

2,加锁规则

  1. 加锁的基本单位是(next-key lock),前开后闭
  2. 插入过程中访问的对象会加锁
  3. 索引上的等值查询:
    1. 给唯一索引加锁时,next-key lock 升级为行锁
    2. 向右遍历时最后一个值不满足查询需求,next-key lock 退化为间隙锁
  4. 唯一索引上的范围查询会访问到不满足条件的第一个值为止

    3,锁的相关语法

    1,表锁

    1,查看加锁的表

    image.png

    2,给指定表加读锁(READ)

    1. ## 加读锁
    2. LOCK TABLE book READ
    加锁之后:
  • 加锁的会话:
    • 加锁的表:
      • 读操作(✔)
      • 写操作(❌)
    • 其他的表:
      • 读操作(❌)
      • 写操作(❌)
  • 其他会话:

    • 加锁的表:
      • 读操作(✔)
      • 写操作(❌),会被阻塞直到锁释放掉
    • 其他的表
      • 读操作(✔)
      • 写操作(✔)

        3,给指定表加写锁(WRITE)

        1. ## 加写锁
        2. LOCK TABLE book WRITE
        加锁之后:
  • 加锁的会话:

    • 加锁的表:
      • 读操作(✔)
      • 写操作(✔)
    • 其他的表:
      • 读操作(❌)
      • 写操作(❌)
  • 其他会话:

    • 加锁的表:
      • 读操作(❌),会被阻塞直到锁释放
      • 写操作(❌),会被阻塞直到锁释放掉
    • 其他的表
      • 读操作(✔)
      • 写操作(✔)

        4,释放表锁

        1. UNLOCK TABLES

        5,分析表锁定的情况

        2,行锁

        1,给行数据加锁

        1,增删改 SQL 加行锁

        InnoDB 对行数据加锁是自动的,如果测试用的话需要把自动提交关闭掉,然后手动进行 commit 和 rollback,关闭操作如下:
  • SET autocommit = 0;

  • START TRANSACTION;
  • BEGIN;

    1. SET autocommit = 0;

    2,查询 SQL 加行锁

    也是要关闭自动提交的

    1. SET autocommit = 0;
    2. SELECT * FROM T FOR UPDATE

    2,释放行锁

    InnoDB 释放行锁是通过事务的操作进行的, commit 和 rollback 都可以释放行数据的锁

    3,行锁的注意事项

  • 如果 SQL 语句没有使用到索引,则行锁会转为表锁

  • 间隙锁(特殊的行锁):如果语句内部

    4,分析行锁

    image.png

    3,间隙锁

    4,MVCC 多版本并发控制

    mvcc(Mluti-Version-Concurrency-Control)是一种并发控制的方法,一般在数据库系统中实现对数据库的并发访问。
    在Mysql的InnoDB引擎中就是指在已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于SELECT操作会访问版本链中的记录的过程。

    1,版本链

    在聚集索引中,InnoDB 在每行数据后面追加了两个隐藏列:

  • trx_id:用来存储对该条记录进行修改的事务id

  • roll_pointer:记录上个版本的数据在 UndoLog 中的位置,因为每次对记录进行修改时,都会把老版本记录到 UndoLog

七,MySql 的锁机制 - 图3
比如现在有个事务id是60的执行的这条记录的修改语句
七,MySql 的锁机制 - 图4
此时在undo日志中就存在版本链
七,MySql 的锁机制 - 图5

2,ReadView

当在事务中执行查询时,会生成一个当前行的 ReadView,它是一个列表,记录着现在正在对该行数据进行操作且未提交事务 id,读已提交和可重复读的区别就是 ReadView 的生成策略不同:

  • 读已提交:每次查询时生成新的 ReadView
  • 可重复读:在事务的首次查询时生成一个 ReadView,之后事务内每次查询都使用这个 ReadView

在列表之的事务 id 是不可访问的!
假设 ReadView 中的事务id 为 [80,100]

  • 如果要访问的记录版本的事务 id 为 70,不在该区间且小于最小事务 id:80,说明这个事务处于当前事务之前且已提交,对当前事务来说该版本数据可访问
  • 如果要访问的记录版本的事务 id 为 90,在该区间,则去查找列表是否存在该 id,如果在说明该事务未提交,即不可访问,如果不在说明已提交可访问
  • 如果要访问的记录版本的事务 id 为 110,不在该区间且大于最大事务 id:100,说明这个事务晚于 ReadView 的生成,所以不可访问。

    3,案例

    比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是
    七,MySql 的锁机制 - 图6
    那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。
    这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。
    那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务
    七,MySql 的锁机制 - 图7
    这时候版本链就是
    七,MySql 的锁机制 - 图8
    这时候之前那个select事务又执行了一次查询,要查询id为1的记录。
    这个时候关键的地方来了
    如果你是已提交读隔离级别,这时候你会重新一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。
    按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是小明2。
    如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。所以第二次select结果和第一次一样,所以叫可重复读!
    也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。