一文详解MySQL各种锁及MVCC的使用场景及原理

目录

一、MySQL锁

1.1 概述

幻读问题

锁的分类

MySQL不同隔离级别的底层实现

锁的释放时机

1.2 全局锁

使用方式

使用场景

改进方式

1.3 表级锁

表锁

使用方式

缺点

元数据锁(MDL)

使用方式

意向锁

AUTO-INC 锁

1.4 行级锁

记录锁(Record Lock)

间隙锁(Gap Lock)

临键锁(Next-Key Lock)

常见规则

行锁粒度粗化

插入意向锁

1.5 乐观锁与悲观锁

乐观锁

悲观锁

1.6 共享排他锁

1.7 实现原理

二、MVCC

实现原理


MySQL慢查询优化

MySQL索引优化

Mysql日期格式转化问题


一、MySQL锁

1.1 概述

幻读问题

在并发事务场景下,很容易产生幻读等现象,而MySQL的默认存储引擎为InnoDB,其只支持RR(可重复读)隔离级别,因而需要各种锁以及MVCC机制来避免幻读

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

1、执行普通 select,此时会以 MVCC 快照读的方式读取数据在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。

所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的幻读

2、执行 select...for update/lock in share mode、insert、update、delete 等当前读在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!

InnoDB 使用 Next-key Lock来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。

锁的分类

  • 根据锁的生效范围:
    • 全局锁
    • 表级锁(表锁,元数据锁,意向锁,AUTO-INC 锁)
    • 行级锁(记录锁,间隙锁,临键锁)
  • 根据操作类型的维度划分:
    • 读锁:查询数据时使用的锁,
    • 写锁:执行插入、删除、修改、DDL语句时使用的锁
  • 根据互斥性划分:
    • 共享锁(仅支持读)
    • 独占锁(读,写均可独占)
  • 根据思想维度划分:
    • 乐观锁:先尝试执行,失败时再获取锁
    • 悲观锁:先获取锁,然后再执行

MySQL不同隔离级别的底层实现

  • RU级别:读操作不加锁,写操作加排他锁。
  • RC级别:读操作使用MVCC机制,每次SELECT生成快照,写操作加排他锁。
  • RR级别:读操作使用MVCC机制,首次SELECT生成快照,写操作加临键锁。
  • 序列化级别:读操作加共享锁,写操作加临键锁。

锁的释放时机

读未提交:SQL执行完成后释放

可重复读:事务结束后释放

1.2 全局锁

使用方式

  • flush tables with read lock
  • unlock tables

使用场景

全库逻辑备份

改进方式

在支持事务的InnoDB存储引擎下,备份前先开启事务,因为其支持MVCC,所以会先生成快照,就可以一边对快照进行增删改查一边对数据库进行备份

1.3 表级锁

InnoDB支持多粒度锁的存储引擎,它的锁机制是基于聚簇索引实现的,当SQL执行时,如果能在聚簇索引命中数据,则加的是行锁,如无法命中聚簇索引的数据则加的是表锁

  • 表锁
  • 元数据锁(MDL)
  • 意向锁
  • AUTO-INC 锁

表锁

使用方式

  • lock tables t_student read; //表级别的共享锁,读锁;
  • lock tables t_stuent write; //表级别的独占锁,写锁;
  • unlock tables

缺点

粒度太大,建议行锁

元数据锁(MDL)

为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更,事务执行期间,MDL长期持有,当有长事务时,如果写锁被别的读锁占用而导致后续所有读锁也无法生效就会阻塞,导致数据库线程爆满

使用方式

对一张表进行 CRUD 操作时,会自动加 MDL 读锁

对一张表做结构变更操作的时候,会自动加 MDL 写锁

意向锁

当表中有添加行锁时(执行插入、更新、删除操作),会同时添加此表的意向独占锁.当需要用到表锁时,快速判断表中的记录有没有行锁。这样可以防止全表扫描来判断有无行锁

普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读,是无锁的

AUTO-INC 锁

AUTO-INC 锁是特殊的表锁机制,锁不是在一个事务提交后才释放,而是执行完插入语句后就会立即释放

在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。

那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。

1.4 行级锁

InnoDB会将表数据存储在聚簇索引中,每条行数据都会存储在树中的叶子节点上,因此行数据是独立的,所以可以对每一条数据上锁,但其他引擎大部分都不支持聚簇索引,表数据都是一起存储在一块的,所以只能基于整个表数据上锁,这也是为什么其他引擎不支持行锁的原因。

  • 普通的 select 语句是不会对记录加锁的,因为它属于快照读
  • 如果要在查询时对记录加行锁,则为锁定读,方式为:
    • select ... lock in share mode;//对读取的记录加共享锁
    • select ... for update;//对读取的记录加独占锁

共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥

开启事务的情况下,查询使用FOR UPDATE,如果使用了索引/主键并且索引生效的情况下,是行锁,否则是表锁

记录锁(Record Lock)

作用于一条记录,只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

间隙锁(Gap Lock)

锁定一个范围(左开右开),但是不包含记录本身

临键锁(Next-Key Lock)

Record Lock+Gap Lock ,锁定一个范围(左开右闭),并且锁定记录本身,用于解决幻读,

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

在能使用记录锁或者间隙锁就能避免幻读现象的场景下,next-key lock 就会退化成退化成记录锁或间隙锁。

常见规则

原则 1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。
原则 2:只有访问到的对象才会加锁。
优化 1:索引上的等值查询,
    命中唯一索引,退化为行锁。
    命中普通索引,左右两边的GAP Lock + Record Lock。
优化 2:
    索引上的等值查询,未命中,所在的Net-Key Lock,退化为GAP Lock 。
索引+范围查询: 
    1.等值和范围分开判断。
    2.索引在范围查询的时候 都会访问到所在区间不满足条件的第一个值为止。
    3.如果使用了倒叙排序,按照倒序排序后,检索范围的右边多加一个GAP。
        哪个方向还有命中的等值判断,再向同方向拓展外开里闭的区间。

行锁粒度粗化

  • 在内存中专门分配了一块空间存储锁对象,当该区域满了后,会将行锁粗化为表锁。
  • 进行范围性写操作时,由于要加的行锁较多,此时行锁开销会较大,会粗化成表锁。

插入意向锁

一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。

如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态

1.5 乐观锁与悲观锁

乐观锁

在MySQL中则可以通过version版本号+CAS的形式实现乐观锁,即在表中多设计一个version字段,然后在SQL修改时以如下形式操作:

UPDATE ... SET version = version + 1 ... WHERE ... AND version = version;

一般的乐观锁都会配合轮询重试机制,比如上述T1执行失败后,再次执行相同语句,直到成功为止。

这种形式却并不适合所有情况,比如写操作的并发较高时,就容易导致一个事务长时间一直在重试执行,从而导致客户端的响应尤为缓慢。因此乐观锁更加适用于读大于写的业务场景,频繁写库的业务则并不适合加乐观锁。

悲观锁

数据库中的排它锁:for update

1.6 共享排他锁

SMO问题:

在SQL执行期间一旦更新操作触发B+Tree叶子节点分裂,那么就会对整个B+Tree加排它锁,这不但阻塞了后续这张表上的所有的更新操作,同时也阻止了所有试图在B+Tree上的读操作,也就是会导致所有的读写操作都被阻塞,影响巨大

为了解决这种情况,MySQL5.7引入共享排他锁,只锁定B+Tree的某个分支,而并不是锁定整颗B+树,从而做到不影响其他分支上的读写操作

1.7 实现原理

(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析 - 掘金 (juejin.cn)

二、MVCC

MVCC(多版本并发控制Multi-Version Concurrency Control):每个连接到数据库的读者,在某个瞬间看到的是数据库的一个快照,写者写操作造成的变化在写操作完成之前(或者数据库事务提交之前)对于其他的读者来说是不可见的。每行数据都存在一个版本号,每次数据更新时都更新该版本

MVCC=隐藏字段+Undo-log+ReadView

MVCC只能在两个隔离级别使用,区别在于:生成ReadView的时机不同

READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView

而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,数据的可重复读其实就是ReadView的重复使用

实现原理

通过在每一行数据中额外保存两个隐藏的列实现

最近更新的事务ID - TRX_ID(6Bytes)+回滚指针 - ROLL_PTR(7Bytes)

当前行创建时的版本号(最近更新的事务ID)删除时的版本号(回滚指针)。这里的版本号并不是实际的时间值,而是系统版本号。每开始新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较。每个事务又有自己的版本号,这样事务内执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的。

首先会去获取表中行数据的隐藏列,然后经过上述一系列判断后,可以得知:目前查询数据的事务到底能不能访问最新版的数据

如果能,就直接拿到表中的数据并返回,

反之,不能则去Undo-log日志中获取旧版本的数据返回。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值