MySQL 的锁机制,那么多的锁,该怎么区分?

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: MySQL 的锁机制,那么多的锁,该怎么区分?


楔子



本篇文章来聊一下 MySQL 的锁,首先不光是数据库,任何的一门高级语言也都内置了锁机制。从本质上讲,锁是一种协调多个进程或多个线程对某一资源进行访问的机制。

而之所以要存在锁,是因为在并发编程中,程序的某一部分在并发访问的时候会导致意想不到的结果。所以这部分程序就需要用锁保护起来,而保护起来的部分就叫做临界区

在 MySQL 中,按照不同的角度,可以将锁分为如下几种:

么多的锁,我们该怎么区分呢?下面就来逐一回答。


脏写是如何避免的



在区分锁之前,先来回顾一个问题,前面我们说四种隔离级别,无论哪一种都可以避免脏写的问题。但怎么避免的当时却没有解释,原因就是涉及到了锁,下面来解释一下。

再来回顾一下什么是脏写,假设事务 A 和 事务 B 同时对张三的账户余额进行更新,初始值为 100,那么两个事务拿到的也都是 100。然后事务 A 给余额增加 100,事务 B 给余额增加 200。理论上最终应该是 400 才对,但如果 A 先提交 B 后提交,最终的结果却是 300;B 先提交 A 后提交,最终的结果就是 200。

以上这种现象就是脏写,具体表现为:两个更新同一条数据,后提交的事务将先提交的事务所做的更新给覆盖了。

那么如何避免呢?显然要依赖锁,多个事务在更新同一条数据的时候要串行更新。

所以当事务在更新数据的时候,会先看这条数据有没有人加锁。如果没有,那么该事务就会创建一个锁,里面包含了事务 ID(trx_id) 和等待状态,然后将锁和这条数据关联在一起。

事务 A 更新数据的时候,会给数据加锁,然后别的事务就不能再更新了。但假设这个时候又来个事务 B 也要更新这条数据,它会怎么做呢?

首先还是判断数据有没有人加锁,结果发现被事务 A 加锁了,就知道自己不能修改这条数据。但事务 B 仍会对这条数据加锁,只不过它处于等待状态。

当事务 A 更新完数据,就会将自己的锁释放掉,并且还会去找,有没有别人也对这条数据加了锁。显然它会发现该数据也被事务 B 加锁了,于是会把事务 B 锁里的等待状态修改为 false,然后唤醒事务 B 开始执行,此时事务 B 就获取到锁了。

以上就是 MySQL 锁机制的一个最基本的原理,其实就和 Python 里面的互斥锁是一样的,但是基于此我们又引申出了很多不同种类的锁。


MySQL 的读锁和写锁



先来聊聊读锁和写锁,读锁也被称为共享锁、S 锁,写锁也被称为独占锁、排它锁、X 锁。而上面多个事务在更新数据时加的锁,就是写锁。

那么问题来了,如果一个事务在读数据的时候,发现这条数据被加锁了,那么该事务需要继续加锁吗?如果是更新数据,那么需要加锁,但读数据是不需要的。因为默认情况下,如果是读数据,会走 MVCC 机制。

因为读数据可以根据 ReadView 在 undo log 版本链里找一个能读取的版本,完全不用考虑是否有别的事务在更新,ReadView 机制不允许当前事务读取别的事务已经更新的值。所以默认情况下读数据完全不需要加锁,更不需要关心别的事务是否在更新数据,直接基于 MVCC 机制读某个快照即可。

但如果就是想在读数据的时候加锁呢?答案是使用读锁,也叫 S 锁、共享锁。

SELECT * FROM girl
WHERE age > 16 IN SHARE MODE;

在查询语句后面加上 IN SHARE MODE 就代表查询数据的时候施加读锁。

注意:读锁和写锁是互斥的,只能有一把写锁或者任意多把读锁,也就是说如果先施加了写锁,就不能再施加读锁,因为两者互斥,当然更不能施加写锁,因为写锁只能有一把。如果先施加了读锁,那么不能再施加写锁,但是可以继续施加读锁,因为读锁可以有任意把。

所以可以得到如下结论:

  • 更新数据的时候必然加写锁(MySQL 自动加),写锁和写锁是互斥的,此时别人不能更新;并且也不能加读锁,因为写锁和读锁也是互斥的;但可以查询,因为查询默认是不加锁的,它走的是 MVCC 机制,会读取快照版本;
  • 查询数据的时候可以加读锁,但需要手动加,默认不加锁。并且读锁和写锁是互斥的,施加了读锁就不能再加写锁,但读锁和读锁之间是不互斥的,可以有任意把读锁;

不过说实话,一般开发业务系统的时候,主动给查询加读锁是很少见的。另外,我们说查询的时候默认没有锁,走的是 MVCC,但可以手动加读锁。其实除了读锁,查询的时候还可以手动加写锁。

SELECT * FROM girl
WHERE age > 16 FOR UPDATE;

在查询语句后面加上 FOR UPDATE 则表示给该查询语句施加写锁,一般主要出现在事务查询完毕之后还要更新数据的时候。比如该数据非常重要,事务在处理的时候不希望受到干扰。而一旦查询的时候加了写锁,那么在事务提交之前,任何人都不能更新数据了,只能在当前事务里更新数据。而等该事务提交之后,别人才能继续更新

另外,读锁也被称为共享锁和 S 锁,写锁也被称为排它锁、独占锁和 X 锁。这里我们一直说的是读锁和写锁,但在 MySQL 中更常说共享锁和独占锁(排它锁),当然意思都是一样的,我们理解就好。


MySQL 的行锁、表锁和页面锁



基于操作类型,我们将锁分为读锁和写锁,如果基于操作的数据粒度划分的话,还可以将锁分为行锁、表锁和页面锁。

像 IN SHARE MODE 和 FOR UPDATE 施加的都属于行锁,因此也可以说行级读锁行级写锁。行锁是针对指定行进行加锁,比如:

-- 更新数据,MySQL会自动施加写锁
-- 并且只对 id = 1 的行施加写锁
-- 其它行不受影响
UPDATE * FROM girl
SET age = age + 1 
WHERE id = 1;

行锁的特点是开销比较大,加锁速度慢,可能会出现死锁,但锁定的粒度最小,发生锁冲突的概率最小,并发度最高。

而表锁则是在整个数据表上对数据进行加锁和释放锁,特点是开销比较小,加锁速度快,一般不会出现死锁,但锁定的粒度比较大,发生锁冲突的概率最高,并发度最低。

在 MySQL 中可以通过以下方式手动添加表锁:

-- 为 account 表增加表级读锁
lock table account read;
-- 为 account 表增加表级写锁
lock table account write;
-- 查看数据表上增加的锁
show open tables;
-- 删除添加的表锁
unlock tables;

但说实话,在工作中我们几乎不会使用表锁,好端端的锁整张表干什么。

最后是页面锁,也称为页级锁,就是在页级别对数据进行加锁和解锁。锁定的粒度介于表锁和行锁之间,并发度一般。

工作中最常用的是行锁,表锁和页面锁基本不用,MySQL 也不会自动添加。但使用行锁的时候,有以下几点需要注意:

  • 行锁主要加在索引上,如果以非索引字段作为条件进行更新,行锁可能会变成表锁;
  • InnoDB 的行锁是针对索引加锁,不是针对记录加锁,并且加锁的索引不能失效,否则行锁可能会变成表锁;

另外行锁、表锁和页面锁都是 InnoDB 存储引擎的特性,可能有人觉得执行 ALTER TABLE 之类的 DDL 语句施加的也是表锁,虽然 DDL 语句和普通的增删改语句之间也是互斥的。但其实 DDL 语句执行时施加的不是表锁,而是元数据锁(metadata locks),这一点要注意。


死锁的产生和预防



虽然锁在一定程度上能够解决并发问题,但稍有不慎,就可能造成死锁。发生死锁的必要条件有 4 个,分别为互斥条件、不可剥夺条件、请求与保持条件和循环等待条件,如下图所示。

1)互斥条件

在一段时间内,计算机中的某个资源只能被一个进程占用,此时如果其他进程请求该资源,则只能等待。

2)不可剥夺条件

某个进程获得的资源在使用完毕之前,不能被其他进程强行夺走,只能由获得资源的进程主动释放。

3)请求与保持条件

进程已经获得了至少一个资源,又要请求其他资源,但请求的资源已经被其他进程占有,此时请求的进程就会被阻塞,并且不会释放自己已获得的资源。

4)循环等待条件

系统中的进程之间相互等待,同时各自占用的资源又会被下一个进程所请求。例如有进程 A、进程 B 和进程 C 三个进程,进程 A 请求的资源被进程 B 占用,进程 B 请求的资源被进程 C 占用,进程 C 请求的资源被进程 A 占用,于是形成了循环等待条件。

但需要注意的是,只有 4 个必要条件都满足时,才会发生死锁。而处理死锁有 4 种方法,分别为预防死锁、避免死锁、检测死锁和解除死锁。

  • 预防死锁:处理死锁最直接的方法就是破坏造成死锁的 4 个必要条件中的一个或多个,以防止死锁的发生。
  • 避免死锁:在系统资源的分配过程中,使用某种策略或者方法防止系统进入不安全状态,从而避免死锁的发生。
  • 检测死锁:这种方法允许系统在运行过程中发生死锁,但是能够检测死锁的发生,并采取适当的措施清除死锁。
  • 解除死锁:当检测出死锁后,采用适当的策略和方法将进程从死锁状态解脱出来。


在实际工作中,通常采用有序资源分配法和银行家算法这两种方式来避免死锁,有兴趣可自行了解一下。


MySQL 的死锁问题



在 MySQL 5.5.5 及以上版本中,默认存储引擎是 InnoDB。该存储引擎使用的是行级锁,在某种情况下会产生死锁问题,所以 InnoDB 存储引擎采用了一种叫作等待图(wait-for graph)的方法来自动检测死锁,如果发现死锁,就会自动回滚一个事务。我们举例说明:

第一步:在终端 1 中将事务隔离级别设置为可重复读,开启事务后为 account 数据表中 id 为 1 的数据添加排他锁。

第二步:在终端 2 中将事务隔离级别设置为可重复读,开启事务后为 account 数据表中 id 为 2 的数据添加排他锁。

第三步:在终端 1 中为 account 数据表中 id 为 2 的数据添加排他锁。

select * from account 
where id = 2 for update;

此时事务 1 会阻塞住,因为它在等待事务 2 释放 id  = 2 的排他锁。

第四步:在终端 2 中为 account 数据表中 id 为 1 的数据添加排他锁。

我们看到死锁了,事务 1 因事务 2 已经处于阻塞了,但此时事务 2 又因事务 1 陷入阻塞,因此出现了循环等待,所以事务 2 直接报错、并且终止。而一旦事务 2 终止,那么它施加的行锁就会失效,然后事务 1 就会给 id = 2 施加行锁成功,不再阻塞。

我们可以通过如下命令查看死锁的日志信息:show engine innodb status \G,或者通过配置 innodb_print_all_deadlocks(MySQL 5.6.2 版本开始提供)参数为 ON,将死锁相关信息打印到 MySQL 错误日志中。



本文参考自:

  • 儒猿技术窝《MySQL 实战高手》
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
存储 关系型数据库 MySQL
MySQL MVCC全面解读:掌握并发控制的核心机制
【10月更文挑战第15天】 在数据库管理系统中,MySQL的InnoDB存储引擎采用了一种称为MVCC(Multi-Version Concurrency Control,多版本并发控制)的技术来处理事务的并发访问。MVCC不仅提高了数据库的并发性能,还保证了事务的隔离性。本文将深入探讨MySQL中的MVCC机制,为你在面试中遇到的相关问题提供全面的解答。
172 2
|
2月前
|
缓存 关系型数据库 MySQL
MySQL并发支撑底层Buffer Pool机制详解
【10月更文挑战第18天】在数据库系统中,磁盘IO操作是性能瓶颈之一。为了提高数据访问速度,减少磁盘IO,MySQL引入了缓存机制。其中,Buffer Pool是InnoDB存储引擎中用于缓存磁盘上的数据页和索引页的内存区域。通过缓存频繁访问的数据和索引,Buffer Pool能够显著提高数据库的读写性能。
129 2
|
2月前
|
SQL 关系型数据库 MySQL
MySQL 锁
MySQL里常见的几种锁
57 3
|
3月前
|
canal 消息中间件 关系型数据库
Canal作为一款高效、可靠的数据同步工具,凭借其基于MySQL binlog的增量同步机制,在数据同步领域展现了强大的应用价值
【9月更文挑战第1天】Canal作为一款高效、可靠的数据同步工具,凭借其基于MySQL binlog的增量同步机制,在数据同步领域展现了强大的应用价值
800 4
|
2月前
|
存储 关系型数据库 MySQL
优化 MySQL 的锁机制以提高并发性能
【10月更文挑战第16天】优化 MySQL 锁机制需要综合考虑多个因素,根据具体的应用场景和需求进行针对性的调整。通过不断地优化和改进,可以提高数据库的并发性能,提升系统的整体效率。
113 1
|
2月前
|
关系型数据库 MySQL Java
MySQL数据锁:Record Lock,Gap Lock 和 Next-Key Lock
本文基于 MySQL 8.0.30 版本及 InnoDB 引擎,深入解析三种行锁机制:记录锁(Record Lock)、间隙锁(Gap Lock)和临键锁(Next-key Lock)。记录锁锁定索引记录,确保事务唯一修改;间隙锁锁定索引间的间隙,防止新记录插入;临键锁结合两者,锁定范围并记录自身,有效避免幻读现象。通过具体示例展示了不同锁的作用机制及其在并发控制中的应用。
227 2
|
2月前
|
存储 关系型数据库 MySQL
MySQL数据库锁:共享锁和独占锁
本文详细介绍了`InnoDB`存储引擎中的两种行级别锁:共享锁(S锁)与排他锁(X锁)。通过具体示例展示了这两种锁的工作机制及其在`InnoDB`与`MyISAM`引擎中的表现差异。文章还提供了锁的兼容性矩阵,帮助读者更好地理解锁之间的互斥关系。最后总结了两种锁的特点及适用场景。适合希望深入了解`MySQL`并发控制机制的读者阅读。
91 1
|
3月前
|
监控 关系型数据库 MySQL
MySQL锁机制与解决死锁问题
MySQL锁机制与解决死锁问题
338 5
|
2月前
|
存储 关系型数据库 MySQL
MySQL锁,锁的到底是什么?
【10月更文挑战第16天】MySQL 锁锁定的是与数据和资源相关的对象,其目的是为了保证数据的一致性、避免冲突,并在并发环境下合理协调事务或操作的执行。理解锁的对象和意义对于优化数据库性能、处理并发问题至关重要。
79 0
|
2月前
|
关系型数据库 MySQL 数据库
mysql锁详解
通过理解并合理运用MySQL中的锁机制,开发者可以有效管理数据库并发访问,平衡性能与数据一致性需求。更多关于MySQL锁的深入探讨和最佳实践,请参考专业的数据库管理资源[[深入MySQL锁机制详解
57 0