MySQL数据库——锁 + MVCC

有错误的地方,欢迎大家评论留言指正!

锁是一种常见的并发事务的控制方式。
MyISAM 仅仅支持表级锁(table-level locking),一锁就锁整张表,这在并发写的情况下性能非常差。InnoDB 不仅支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。
行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。

一、表级锁和行级锁对比 :

表级锁:

MySQL中锁定粒度最大的一种锁(全局锁除外),对当前操作的整张表加锁,实现简单,加锁的开销小,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM和 InnoDB 引擎都支持表级锁。

行级锁:

MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。行级锁能大大减少数据库操作的冲突。并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。

注意:当我们执行 UPDATE、DELETE 语句时,如果 WHERE条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁。不过,很多时候即使用了索引也有可能会走全表扫描,这是因为 MySQL 优化器的原因。

二、InnoDB 有哪几类行锁?

InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式:

  • 记录锁(Record Lock) :
    • 属于单个行记录上的锁。
  • 间隙锁(Gap Lock) :
    • 锁定一个范围,不包括记录本身。对于键值在条件范围内但并不存在的记录。
  • 临键锁(Next-Key Lock) :
    • Record Lock+Gap Lock,锁定一个范围,包含记录本身。主要目的是为了解决幻读问题(MySQL事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。

三、共享锁和排他锁:

不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类:

共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁, 允许多个事务同时获取(锁兼容)。
排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁, 不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。

由于 MVCC 的存在,对于一般的 SELECT 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。

  • SELECT … LOCK IN SHARE MODE; // 共享锁
  • SELECT … FOR SHARE;// 共享锁
  • SELECT … FOR UPDATE; // 排他锁

四、意向锁:

用来快速判断是否可以对某个表使用表锁。 如果需要用到表锁的话,如何判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东西。
意向锁是表级锁,共有两种:

意向共享锁(Intention Shared Lock,IS 锁): 事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
意向排他锁(Intention Exclusive Lock,IX 锁): 事务有意向对表中的某些记录加排他锁(X锁),加排他锁之前必须先取得该表的 IX 锁。

意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
意向锁之间相互兼容。意向锁不会和行级锁冲突;意向排他锁与共享锁和排它锁互斥;意向共享锁与排它锁互斥 (这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。

在这里插入图片描述

在这里插入图片描述
事务A在申请行锁(写锁)之前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的写锁时就会失败,因为表上有意向排他锁之后事务B申请表的写锁时会被阻塞。

五、死锁:

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。 当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。
死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。
为了解决这种问题,数据库系统实现了各种死锁检测死锁超时机制。

六、MVCC:

MVCC 是多版本并发控制方法, 用于在多个并发事务同时 读写数据库时保持数据的一致性和隔离性。 它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。
MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

一致性非锁定读和锁定读:

  • 一致性锁定读(当前读):(就是给行记录加 X 锁或 S 锁)

    • 锁定读,也称为当前读(current read):每次读取的是数据的最新版本,锁定读会对读取到的记录加锁。 如下列语句:

    SELECT … FOR SHARE / LOCK IN SHARE MODE:当前读方式,对记录加 S 锁,其它事务也可以加S锁,如果加 x 锁则会被阻塞。
    SELECT … FOR UPDATE / insert、update、delete:当前读方式,对记录加 X 锁,且其它事务不能加任何锁。

    • InnoDB 存储引擎中,在 Repeatable Read 下,如果是当前读 ,每次读取的都是最新数据,会对读取的记录使用 Next-key Lock 临键锁来防止幻读。
      • 这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以,会对读取的记录使用 Next-key Lock 临键锁,来防止其他事务在间隙间插入数据。
  • 一致性非锁定读(快照读、MVCC):(就是单纯的 SELECT 语句)

    • 对于 一致性非锁定读(Consistent Nonlocking Reads)的实现,每次读取的是数据的可见版本;通常做法是加一个版本号或者时间戳字段;InnoDB 中事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的行记录的版本号进行对比。

    更新时:数据的版本号 + 1 或者更新时间戳。
    查询时:将当前可见的版本号与查询到的行记录的版本号进行比对,如果查询到的记录的版本号小于可见版本,则表示该记录可见

    • InnoDB 存储引擎中,在 Repeatable Read 下 MVCC 实现了可重复读和防止部分幻读(或者说减少了幻读,快照读无法在事务执行期间检测到其他事务的插入或删除操作)。如果读取的行正在执行 DELETE 或 UPDATE 操作(即读取的记录已被其它事务加上 X 锁),这时读取操作不是去等待行上锁的释放。而是读取行的一个快照数据,只能读取到第一次查询之前所插入的数据(根据 Read View 判断数据可见性,Read View 在第一次查询时生成)

RC 和 RR 隔离级别下 MVCC 的差异(InnoDB中,快照读包括:可重复读和读已提交两种):

在事务隔离级别 RC 和 RR (InnoDB 存储引擎的默认事务隔离级别)下,InnoDB 存储引擎使用 MVCC(非锁定一致性读)时,但它们生成 Read View 的时机却不同:

在 RC 隔离级别: 每次select 查询前都生成一个Read View (m_ids 列表);读取的是已提交的最新版本数据。
在 RR 隔离级别: 只在事务开始后 第一次select 数据前生成一个Read View(m_ids 列表);读取的是数据的旧版本(事务开始时)。

MVCC + Next-key-Lock 防止幻读:

InnoDB 存储引擎在 RR 级别下通过 MVCC 和 Next-key-Lock 来解决幻读:

  • 1、执行普通的 select,此时会以 MVCC 快照读的方式读取数据
    在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。
    实现了可重复读和防止快照读下的 “幻读”
  • 2、执行 select…for update/for share/lock in share mode、insert、update、delete 等当前读
    如果是当前读 ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以,会对读取的记录使用 Next-key Lock 临键锁,来防止其他事务在间隙间插入数据。

MVCC 在 MySQL 中实现所依赖的手段主要是:

隐藏字段

在这里插入图片描述

  • DB_TRX_ID(6字节): 表示最后一次插入或更新该行的事务 id。 此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除。
    • 新增一条数据时 DB_TRX_ID 默认为 1,修改时数值加 1,一个自增的数值。
  • DB_ROLL_PTR(7字节): 回滚指针,指向该行记录的上一个版本。 如果该行未被更新,则为空
  • DB_ROW_ID(6字节): 隐藏主键, 如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引

undo log

  • 回滚日志,记录某行数据的多个版本,在增删改时产生,回滚时用于将数据恢复到修改前的样子。

    • insert 时:产生的日志只在回滚时需要,事务提交后可被立即删除。
    • update、delete 时:产生的日志在回滚、MVCC 版本访问时需要,不会被立即删除。
  • undo log 版本链MySQL数据库——锁 + MVCC_第1张图片

read view

  • read view(读视图)是快照读 SQL 执行时 MVCC 提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。

  • read view 中包含的四个核心字段:在这里插入图片描述

    • m_ids: 记录当前活跃的事务 id 集合,未提交的事务 id。
    • min_trx_id: 记录最小的活跃事务 id。
    • max_trx_id: 预分配事务 id,当前最大事务 id+1。
    • creator_trx_id: 创建 read view 的事务 id。

以 RC 隔离级别为例,快照读原理:
MySQL数据库——锁 + MVCC_第2张图片

参考:
高性能MySQL(第三版)
原文链接:https://javaguide.cn/database/mysql/innodb-implementation-of-mvcc.html
原文链接:https://javaguide.cn/database/mysql/mysql-questions-01.html
原文链接:https://www.bilibili.com/video/BV1yT411H7YK

你可能感兴趣的:(mysql,数据库,java)