天天看点

一文带你聊聊MYSQL的锁和MVCCLBCC(单版本控制-锁)MVCC(多版本控制)什么是幻读?

如果你觉得内容对你有帮助的话,不如给个赞,鼓励一下更新😂。
本文内容总结自极客时间《MySQL实战45讲》专栏

LBCC(单版本控制-锁)

基于锁的并发控制,这种方案比较简单粗暴,就是一个事务去读取一条数据的时候,就上锁,不允许其他事务来操作(当然这个锁的实现也比较重要,如果我们只锁定当前一条数据依然无法解决幻读问题)。

在 MySQL 事务中,锁的实现与隔离级别有关系,在 RR(Repeatable Read)隔离级别下,MySQL 为了解决幻读的问题,以牺牲并行度为代价,通过 Gap 锁来防止数据的写入,而这种锁,因为其并行度不够,冲突很多,经常会引起死锁。现在流行的 Row 模式可以避免很多冲突甚至死锁问题,所以推荐默认使用 Row + RC(Read Committed)模式的隔离级别,可以很大程度上提高数据库的读写并行度。

根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。

全局锁

全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是

Flush tables with read lock (FTWRL)

。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。

全局锁的典型使用场景是,做全库逻辑备份。 也就是把整库每个表都 select 出来存成文本。

表级锁

开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 会发生在MyISAM、memory、InnoDB、BDB 等存储引擎中。

MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。

表锁

表锁的语法是 lock tables … read/write。 与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。

元数据锁

MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是防止DDL和DML并发的冲突,保证读写的正确性。在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。

  • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。​

注意:MDL 会直到事务提交才释放,在做表结构变更的时候,你一定要小心不要导致锁住线上查询和更新。

online DDL

Online DDL的过程是这样的:

  1. 拿MDL写锁
  2. 降级成MDL读锁
  3. 真正做DDL
  4. 升级成MDL写锁
  5. 释放MDL锁

1、2、4、5如果没有锁冲突,执行时间非常短。第3步占用了DDL绝大部分时间,这期间这个表可以正常读写数据,是因此称为“online ”

行锁

锁定粒度最小,发生锁冲突的概率最低,并发度最高。会发生在InnoDB 存储引擎。

S or X (共享锁、排他锁)

数据的操作其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操作使用不同的锁;InnoDB 实现了标准的行级锁,也就是共享锁(Shared Lock)和排他锁(Exclusive Lock)。

  • 共享锁(S):多个事务可以一起读,共享锁之间不互斥,共享锁会阻塞排它锁,可以通过 lock in share mode 语句显示使用共享锁。
  • 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享锁和排他锁。对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会自动给涉及的数据集加排他锁,或者使用 select * for update 显示使用排他锁。
    • 只有在自动提交被禁用时,

      FOR UPDATE

      才可以锁定行,若开启自动提交,则匹配的行不会被锁定。
    • 拿不到锁的事务会一直阻塞到拿到锁或者锁超时为止,可以通过

      set [global|session] innodb_lock_wait_timeout = 10;

      来设置锁超时时间。
一文带你聊聊MYSQL的锁和MVCCLBCC(单版本控制-锁)MVCC(多版本控制)什么是幻读?

InnoDB 行锁

InnoDB 行锁是通过对索引数据页上的记录(record)加锁实现的,所以InnoDB只有在通过索引条件检索数据时使用行级锁,否则使用表锁。

主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。

  • Record Lock 锁: 单个行记录的锁(锁数据,不锁 Gap)。
  • Gap Lock 锁: 间隙锁,锁定一个范围,不包括记录本身(不锁数据,仅仅锁数据前面的Gap)。
  • Next-key Lock 锁: 同时锁住数据,并且锁住数据前面的 Gap(为了解决幻读问题)。

比如当你执行

select * from t where d=5 for update

的时候,就不止是给数据库中已有的记录加上了行锁,还同时记录之间加了间隙锁。这样就确保了无法再插入新的记录。

也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。

Next-key Lock 锁

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。需要注意的是,每个间隙锁都是前开后开区间。

跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作,也即给相同区间加间隙锁的时候是不冲突的。间隙锁引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”。当互相持有想插入记录的间隙锁时,会发生死锁。并且间隙锁可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”:

  • 原则1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock是前开后闭区间。
  • 原则2:查找过程中访问到的对象才会加锁。
  • 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
  • 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
  • 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

页级锁

开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。会发生在:BDB 存储引擎。

一文带你聊聊MYSQL的锁和MVCCLBCC(单版本控制-锁)MVCC(多版本控制)什么是幻读?

死锁

死锁产生的四个条件是什么呢?

  • 互斥条件:一个资源每次只能被一个进程使用;
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:进程已获得的资源,在没使用完之前,不能强行剥夺;
  • 循环等待条件:多个进程之间形成的一种互相循环等待资源的关系。

如何避免死锁的产生呢?这里给出一些建议:

  • 加锁顺序一致(把最可能造成锁冲突、最可能影响并发度的锁尽量往后放);
  • 尽量基于 primary 或 unique key 更新数据;
  • 单次操作数据量不宜过多,涉及表尽量少;
  • 减少表上索引,减少锁定资源。

两阶段锁(Two-Pahse Locking – 2PL)

两阶段锁协议规定所有的事务应遵守的规则:

  1. 在对任何数据进行读写操作之前,首先要申请并获得对该数据的封锁
  2. 在释放一个封锁之后,事务不再申请和获得其它任何封锁

即事务的执行分为两个阶段:

  1. 第一阶段是获取封锁的阶段,称为扩展阶段
  2. 第二阶段是释放封锁的阶段,称为收缩阶段

首先,两阶段锁强调的是“加锁(增长阶段,growing phase)和解锁(缩减阶段,shrinking phase)这两项操作,且每项操作各自为一个阶段,这就是说不管同一个事务内需要在多少个数据项上加锁,那么所有的加锁操作都只能在同一个阶段完成,在这个阶段内,不允许对对已经加锁的数据项进行解锁操作,即加锁和解锁操作不能交叉执行(同一个事务内)。这一条是说在同一个事务内部的事情。 在InnoDB事务中,行锁在需要的时候才加上,但是并不是不需要了就立马释放,而是要等到事务结束才会释放。

MVCC(多版本控制)

通过乐观锁的方式来解决不可重复读和幻读问题,实际上 MVCC 机制它可以在大多数情况下替代行级锁,降低系统的开销。

注意:MVCC 只在 Read Commited(读已提交) 和 Repeatable Read(可重复读) 两种隔离级别下工作。

MVCC 的英文全称是 Multiversion Concurrency Control,中文翻译过来就是多版本并发控制技术。 从名字中也能看出来,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制,简单来说它的思想就是保存数据的历史版本。这样我们就可以通过比较版本号决定数据是否显示出来。

通过 MVCC 我们可以解决以下几个问题:

  1. 读写之间阻塞的问题,通过 MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
  2. 降低了死锁的概率。这是因为 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
  3. 解决一致性读的问题。一致性读也被称为快照读,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。

什么是幻读?

在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。

在两次并发的会话中,session B 的修改结果,被 session A 之后的 select 语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。

快照读与当前读

在 MVCC 并发控制中,读操作可以分为两类:快照读(Snapshot Read)与当前读 (Current Read)。其中LBCC解决的是当前读情况下的幻读,MVCC解决的是普通读(快照读)的幻读。

快照读

简单的 select 操作,读取的是记录的可见版本(有可能是历史版本),不用加锁。

当前读

特殊的读操作,插入/更新/删除操作,读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发修改这条记录。