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月前
|
canal 消息中间件 关系型数据库
Canal作为一款高效、可靠的数据同步工具,凭借其基于MySQL binlog的增量同步机制,在数据同步领域展现了强大的应用价值
【9月更文挑战第1天】Canal作为一款高效、可靠的数据同步工具,凭借其基于MySQL binlog的增量同步机制,在数据同步领域展现了强大的应用价值
192 4
|
2天前
|
监控 关系型数据库 MySQL
MySQL锁机制与解决死锁问题
MySQL锁机制与解决死锁问题
16 5
|
2天前
|
存储 关系型数据库 MySQL
深入解析MySQL数据存储机制:从表结构到物理存储
深入解析MySQL数据存储机制:从表结构到物理存储
10 1
|
7天前
|
关系型数据库 MySQL Unix
MySQL配置不区分大小写的方法
结论 通过适当配置 lower_case_table_names参数以及在数据定义和查询中选择合适的校对规则,可以灵活地控制MySQL中的大小写敏感性,以适应不同的应用场景和需求。这样的设置既可以增加数据库的兼容性,又可以在必要时利用大小写敏感性进行精确的数据处理。需要注意的是,修改 lower_case_table_names参数后,最好在数据库初始化时进行,以避免现有表名的大小写问题。
24 3
|
24天前
|
关系型数据库 MySQL 数据库
Mysql的锁
本文介绍了MySQL中表级锁和行级锁的区别,其中MyISAM仅支持表级锁,而InnoDB支持表级锁和行级锁,默认为行级锁。表级锁锁定整个表,实现简单,资源消耗少,但并发度低;行级锁仅锁定相关记录,减少冲突,提高并发度,但加锁开销大。此外,还介绍了共享锁和排他锁的概念及意向锁的作用。
|
2月前
|
监控 关系型数据库 MySQL
在Linux中,mysql的innodb如何定位锁问题?
在Linux中,mysql的innodb如何定位锁问题?
|
2月前
|
存储 SQL 关系型数据库
深入MySQL锁机制:原理、死锁解决及Java防范技巧
深入MySQL锁机制:原理、死锁解决及Java防范技巧
|
2月前
|
存储 SQL 关系型数据库
深入解析MySQL事务机制和锁机制
深入解析MySQL事务机制和锁机制
|
2月前
|
SQL 存储 关系型数据库
"MySQL增列必锁表?揭秘InnoDB在线DDL,让你的数据库操作飞一般,性能无忧!"
【8月更文挑战第11天】在数据库领域,MySQL凭借其稳定高效的表现深受开发者喜爱。对于是否会在给数据表添加列时锁表的问题,MySQL的行为受版本、存储引擎等因素影响。从5.6版起,InnoDB支持在线DDL,可在改动表结构时保持表的可访问性,避免长时间锁表。而MyISAM等则需锁表完成操作。例如,在使用InnoDB的表上运行`ALTER TABLE users ADD COLUMN email VARCHAR(255);`时,通常不会完全锁表。虽然在线DDL提高了灵活性,但复杂操作或大表变更仍可能暂时影响性能。因此,进行结构变更前应评估其影响并择机执行。
53 6
|
2月前
|
存储 SQL 关系型数据库
MySQL中的update操作与锁机制
本文探讨MySQL中`UPDATE`操作的自动加锁机制及其对数据一致性的保障作用。尤其在InnoDB存储引擎下,系统会在涉及索引的更新操作中加行锁或间隙锁,防止多事务并发修改同一条记录。通过福利码兑换系统的实例展示,当线程A开启事务更新库存时,线程B试图更新相同记录会被阻塞,直至线程A提交。此外,文章还介绍了乐观锁及版本号控制等策略进一步提升并发性能的方法。作者:小明爱吃火锅,来源:稀土掘金。
137 2