只改一行语句,锁这么多?

news2024/11/25 10:35:51

c6eea934578fb13df50d0943f9bbd6f7.png

e058dd8ffdb921bda58911cb19fe4941.gif

👉导读

这篇文章我想来聊聊 MySQL 的锁是怎么加上的,为啥想聊这个呢?主要是因为业务中我们或多或少都会使用到锁,毕竟锁是保障我们数据安全性的关键法宝。但是由于不了解原理,往往可能导致我们在”刻意“或者”无意“的使用场景下,带来潜在的性能问题,轻则导致处理能力降低,重则可能会拖垮我们的 DB,因此需要对锁的原理以及使用场景有比较全面的了解,才能更好地驾驭,避免给我们带来不必要的业务隐患。

👉目录

1 啥时候加

2 如何加

3 什么时候该加什么时候不该加

我们主要从三个方面来讨论这个问题:

▶︎ 啥时候加?

▶︎ 如何加?

▶︎ 什么时候该加什么时候不该加?

01

啥时候加

072416807de4ad8d52f5053a40f5e51c.jpeg

   1.1 显示锁

MySQL 的加锁可以分为显示加锁和隐式加锁,显示加锁我们比较好识别的,因为他往往直接体现在 SQL 中,常见的显示加锁语句主要有:

▶︎ select ... for update;

▶︎ select ... in share mode;

两者的区别在于前者加的是排它锁,后者加的是共享锁。加了排他锁之后,后续对该范围数据的写和读操作都将被阻塞,另外一个共享锁不会阻塞读取,而是阻塞写入,但是这往往会带来一些问题,比如电商场景下更新库存时候,我们为了保障数据的一致性更新往往需要先将该商品数据锁住,如果此时两个线程并发更新库存,就可能会导致数据更新出现异常。

所以我们在业务上往往会使用 select ... for update 对数据进行加锁。另外还有些咱们比较不常用的加锁方式,比如:

▶︎ 全局锁:Flush tables with read lock,主要在进行逻辑备份的时候会用到

▶︎ 表锁:lock tables … read/write

   1.2 隐式锁

隐式锁是我们需要特别关注的,很多的“坑”就是因为隐式锁的存在导致的,无形往往最为致命。

表级锁除了表锁以外,还有元数据锁:

▶︎ 在进行增删改查的时候会加 MDL 读锁;

▶︎ 在对表结构进行变更的时候,会加 MDL 写锁;

这个会带来的问题就是当我们想给表添加索引或者修改表结构的时候,由于加了 MDL 写锁,会阻塞我们线上正常的读写请求,这个时候可能会触发上游的失败重试机制,那很可能就会出现请求雪崩导致 DB 被打挂。

另外的就是与我们日常业务息息相关的行锁以及间隙锁,当我们在进行增删改的时候,会根据当前的隔离级别加上行锁或者间隙锁,那么这时候需要注意是否会影响正常业务的读写性能,另外带来的风险就是可能出现加锁范围过大阻塞请求,并触发上游重试,导致服务雪崩,DB 打挂。

   1.3 会不会加锁呢?

cd52a6d83efc3f55dc347a5449b6b787.png

谈到这里有的同学可能有疑问,你这增删改都加锁了,那我读的时候岂不是性能很差,特别是在读多写多的业务场景下,我的读请求一上来的话,DB 不是分分钟被我查挂了?其实这里 innodb 引擎用到了一个 mvcc 的技术即多版本并发控制,其原理就是在数据更新的同时在 undolog 中记录更新的事务 id 以及相应的数据,并且维护一个 Readview 的活跃事务 id,这样当一个事务执行的时候,很容易能知道自己能看见什么数据,不能看见什么数据,这时候读取数据自然也就不会受到锁的影响能够正常地读取啦。

02

怎么加

01e4fb1dc836113deed414e0915694a5.gif

这里讨论怎么加其实就是了解加锁的类型以及范围,即用了什么锁且加在哪里了?在讨论这个问题之前我们先来看看事务隔离级别:

▶︎ 读未提交;

▶︎ 读已提交;

▶︎ 可重复读;

▶︎ 串行化;

为啥要说这个呢?因为隔离级别也影响着咱们的加锁,读已提交解决了脏读的问题,但是未解决幻读问题;可重复读通过引入间隙锁解决了幻读问题,因此意味着不同的隔离级别用到的锁还不一样,但是有一点明确的是,越高隔离级别锁的使用更加严格。可重复读是默认的事务隔离级别,但是线上设置的隔离级别往往都是读已提交,主要是因为这个级别够用并且能够有更好的并发性能。接下来我们讨论的范围也主要是在读已提交(RC)和可重复读(RR)。

这里根据相应规则来具体分析:

▶︎ 原则1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。

▶︎ 原则2:查找过程中访问到的对象才会加锁。

▶︎ 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。

▶︎ 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。

▶︎ 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

另外有两点需要注意的是:

▶︎ 锁是加在索引上的;

▶︎ gap锁是共享的而非独占的。

   2.1 RC

接下来分别进行讨论,可能有些冗长,需要你耐心看完。

首先是 RC 级别,这个级别下的加锁规则是比较简单的,因为只涉及到行锁,首先我们先设计一张表

CREATE TABLE `t_db_lock` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`)
) ENGINE=InnoDB;


insert into t_db_lock values(0,0,0),(5,5,5),(10,10,10);
   2.2 主建等值存在

sessionA

sessionB

sessionC

begin;update t_db_lock set id=id+1 where id = 0;




insert into t_db_lock values(1,1,1) [block]




update t_db_lock set id=id+1 where id = 5;[success]

▶︎ 可以看到此时 sessionA 在做主键上的数据更新,将当前的记录的主键值更新为1,此时 db 会在 id=1 和 0 上加上行锁,即此时针对该id的更新会被阻塞;

▶︎ 因此当 sessionB 想插入 id=1 的记录时会被阻塞住;

▶︎ 但是由于 sessionC 更新的是 id=5 的记录,因此可以执行成功。

   2.3 非唯一等值

sessionA

sessionB

sessionC

begin;update t_db_lock set b=b+1 where a = 0;




update t_db_lock set b=b+1 where id = 0; [block]




update t_db_lock set b=b+1 where b = 0;[block] 

▶︎ sessionA 根据普通索引的判断条件更新数据,由于行锁是加在索引上,因此这时候 a 列相关索引数据上了锁;

▶︎ 但是为啥这时候我更新 id=0 的数据也被阻塞了呢?因为这时除了加 a 上的索引,还有回表更新的操作,此时访问到的主键上的索引也会被加锁,因为是同一行,所以此时更新同样被阻塞住;

▶︎ 同样的道理,当我们去更新的 b=0 的数据对应的主键索引上也是同一条数据,所以此时更新也被阻塞,但是如果我们此时是更新 b=5 的这条数据的话就能更新成功。

   2.4 主键等值不存在

sessionA

sessionB

sessionC

begin;select * from t_db_lock where id = 2 for update;




update t_db_lock set b=b+1 where a = 0; [success]




update t_db_lock set b=b+1 where b = 0;[success]

▶︎ sessionA 加了一个 id 为2的锁,此时这行记录不存在,行锁没有加成功,因此不会阻塞其他 session 的请求;

▶︎ sessionB 执行成功;

▶︎ sessionC 执行成功。

   2.5 无索引等值不存在

sessionA

sessionB

sessionC

begin;select * from t_db_lock where b=3 for update;




update t_db_lock set b=b+1 where a = 0; [success]




update t_db_lock set b=b+1 where b = 5;[success]

▶︎ 这种情况和主键等值不存在一致,由于未找到对应的加锁记录,则后续的更新操作都能够执行成功。

   2.6 主键范围

sessionA

sessionB

sessionC

begin;select * from t_db_lock where id >= 0 and id <= 5 for update;




update t_db_lock set b=b+1 where a = 0; [block]




insert into t_db_lock values(1,1,1) [success]

▶︎ sessionA 根据范围加锁,锁了 id=0 和 5 这两行数据;

▶︎ sessionB 由于更新 id=0 这行已经上锁的数据,所以被阻塞住;

▶︎ sessionC 由于之前 id=1 这行记录并不存在,所以可以正常插入,这个场景是不是有点熟悉,就是咱们所说的幻读,如果这时候在 sessionA 中再执行 select * from t_db_lock where id >= 0 and id <= 5 就会发现多了一条数据;

   2.7 RR

这里可重复读级别下主要是讨论间隙锁的加锁场景,这种加锁情况会比读已提交的隔离级别复杂的多;set session transaction isolation level repeatable read。

   2.8 主键等值存在

sessionA

sessionB

sessionC

begin;select * from t_db_lock where id = 5 for update;




insert into t_db_lock values(2,2,2); [success]




insert into t_db_lock values(6,6,6); [success]

▶︎ sessionA 在已经存在的 id=5 这行加锁,根据加锁规则,唯一索引会退化为行锁,因此仅在 id=5 这行加锁;其实这也好理解,既然已经是唯一索引了,那么就不会会出现幻读的情况,因此幻读仅仅取决于这行是否存在,因此我只要给该行加锁保证不再写入即可;

▶︎ sessionB 和 sessionC 均不在锁范围内则插入成功.

   2.9 非唯一等值

sessionA

sessionB

sessionC

begin;select * from t_db_lock where a = 5 for update;




insert into t_db_lock values(6,11,6); [block]




insert into t_db_lock values(11,10,6); [success]

▶︎ sessionA 在已经存在的 a=5 这行记录上加锁,由于是非唯一索引,根据加锁规则,首先扫描 a 索引加上 next-key lock (0,5] ,接着向右遍历到第一个不满足条件的(根据规则五,唯一索引上的范围查询会访问到不满足条件的第一个值为止),并退化为间隙锁,因此加锁范围为(5,10),总体加锁范围为(0,10);并且 for update,也会对应在主键的索引范围内加上锁,即(0,10);

▶︎ sessionB 在主键索引的锁范围内,因此被阻塞;

▶︎ sessionC 此时不在普通索引和主键索引的范围上,因此执行成功;


这里可以看到,对于非唯一等值查询的情况下,加锁的范围要比主键等值存在更大,因此我们在对非唯一索引加锁的时候需要注意这个范围。

   2.10 主键等值不存在

sessionA

sessionB

sessionC

begin;select * from t_db_lock where id = 3 for update;




insert into t_db_lock values(2,2,2); [block]




insert into t_db_lock values(6,6,6); [success]

▶︎ sessionA 此时对 id=3 的记录加上了行锁,但是由于此时3这行的记录不存在,会对此范围加锁,按照加锁原则,向右遍历且最后一个值不满足等值条件,next-key lock 退化为间隙锁,此时加锁范围为(0,5);

▶︎ sessionB 属于加锁范围内,因此被阻塞;

▶︎ sessionC 不在此加锁范围内,加锁成功。

为啥这里要加的是范围锁呢,其实主要解决的是幻读问题,假设这里没有在此范围内加锁,那么 T1 时刻 sessionB 执行成功,T2 时刻再次执行 select * from t_db_lock where id = 3 的话,就会发现原先查询不到的结果现在竟然可以查询到了,就像出现幻觉一样;为了避免出现这种幻读的情况,需要在此范围内加锁。

   2.11 非唯一等值不存在

sessionA

sessionB

sessionC

begin;select * from t_db_lock where a = 3 for update;




insert into t_db_lock values(3,5,5); [block]




insert into t_db_lock values(6,5,5); [success]

▶︎ sessionA 在 a=3 这行上加锁的,由于 db 中不存在该行,所以同样会加next-key lock,并且因为锁都是加在索引上的,因此会在 a 索引上加上(0,5)的范围锁。但是这里有个奇怪的现象,当 a=5 时,如果 id<5 会阻塞,如果 id>5 则会成功,从结果看来,此时 a 上的锁似乎是有偏向性的,并不是严格意义上的 a=5 时就会锁住相应的插入记录

   2.12 主键范围

sessionA

sessionB

sessionC

select * from t_db_lock where id >= 5 and id < 6 for update;




insert into t_db_lock values(3,3,3); [success]




insert into t_db_lock values(10,10,10); [block]

▶︎ sessionA 进行范围查询加锁,在语义上等价于 select * from t_db_lock where id = 5 for update,但是实际加锁情况还是有很大的区别,首先 id >= 5 根据等值查询查询到id=5这行加锁为(0,5],由于是唯一索引,退化为行锁,因此在 id=5 这行上加了锁,接着向右查询,找到第一个不满足条件的值,即 id=10 这行,所以加 next-key lock(5,10],这里因为并不是等值查询,不会有退化为间隙锁的过程,所以整体加锁范围[5,10];

▶︎ sessionB 不在锁范围内,插入成功;

▶︎ sessionC 在锁中,插入失败,注意这里是被阻塞住,而不是报主键冲突。

   2.13 非唯一范围

sessionA

sessionB

sessionC

begin;select * from t_db_lock where a >= 5 and a < 6 for update;




insert into t_db_lock values(3,3,3); [block]




insert into t_db_lock values(10,10,10); [block] 

▶︎ sessionA 加锁范围区别于主键索引主要是在(0, 5]这个范围下并未退化为行锁,因此总体加锁范围为(0, 10]

   2.14 无索引等值不存在

sessionA

sessionB

sessionC

begin;select * from t_db_lock where b = 6 for update;




insert into t_db_lock values(3,3,3); [block]




insert into t_db_lock values(10,10,10); [block]

▶︎ sessionA 中加锁记录为 b=6 这行,由于 b 未创建索引,因此会将所有 b 索引上的记录都加锁,由于是 for update 加锁,认为还回去主表上更新,因此主表的相关记录也都被上了锁,这就会导致加锁期间处于锁表的状态,任何的更新操作都没办法成功,这在线上会是非常危险的操作,可能会导致 db 被打垮。

03

什么时候该加什么时候不该加

通过上述的分析我们应该对锁的类型以及语句中加锁的范围有一个大致的了解,可以知道悲观锁是需要我们谨慎使用的,因为很可能简单的 SQL 就会拖垮 db 的性能,影响线上服务的质量,那么什么时候该加什么时候不该加呢?

我认为对于 db 的并发场景,我们可以这么去考虑:

▶︎ 尽可能优先考虑使用乐观锁的方式解决;

▶︎ 如果需要用到悲观锁,则务必在加锁的键上加索引;

▶︎ 确认 db 的隔离级别,分析 SQL 中可能存在导致冲突或者死锁的原因,避免 SQL 被长时间阻塞;

其实对于 db 的互斥方案并没有银弹,要根据具体的业务场景去针对性的制定解决方案,只是在可能出现的一些坑中,我们能够提前识别到,避免低级错误,并且有能力去优化他,这就是能让自己不断进步提升的好方法啦。

-End-

原创作者|李滨

  07331cfff315a47c9d2567ee82f2d883.png

关于MySQL锁机制你还有什么想聊的?欢迎评论。我们将选取1则最有价值的评论,送出 QQ Family 毛绒公仔1个(见下图)。11月13日中午12点开奖。

71e74f89a80f973e25e47dac76d0dec2.png

📢📢欢迎加入腾讯云开发者社群,社群专享券、大咖交流圈、第一手活动通知、限量鹅厂周边等你来~

4e20ff956511da7c4aabbfdbacb84a96.png

(长按图片立即扫码)


c873067836d000417c2c8360e1b8e517.png

0787259164227df0da60297b93bbbadb.png

dd777667775fbcf00507d85e75be7f46.png

65070cc4db9e2614b545a3e69a66a196.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1179542.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript从入门到精通系列第三十篇:详解JavaScript中的正则表达式语法

文章目录 前言 1&#xff1a;概念回顾 2&#xff1a;正则表达式 一&#xff1a;正则表达式 1&#xff1a;正则表达式字面量 2&#xff1a;检查是否有a或者b 3&#xff1a;检查是否有字母 4&#xff1a;检查是否有abc/aec/afc 5&#xff1a;检查除了ab 大神链接&#x…

Python教程:打印自己的名字

要打印的名字是&#xff1a;PYTHON …######… …#…#… …######… …#… …#… …#…#… …#…#… …##… …##… …##… …######… …##… …##… …##… …##… …#…#… …#…#… …######… …#…#… …#…#… …######… …#…#… …#…#… …#…#… …######… ……

【Spring实战——构建Spring Web应用程序】1.10 处理表单

引言 Web应用功能 ○ 提供内容 ○ 用户填写表单 ○ 提交数据 Spring MVC的控制器提供了 ○ 处理表单展示 ○ 用户提交数据的支持 在Spittr应用中&#xff0c;需要一个注册表单供新用户使用。SpitterController是一个新的控制器&#xff0c;目前只有一个请求处理方法用于展示…

LInux-0.11

文章目录 前言学习资料正文 前言 B站视频链接 linux 0.11 内核代码 学习资料 正文 一个山区512字节

稀土/铜催化剂电催化CO2制C2+或CH4

在电化学CO2还原反应&#xff08;CO2RR&#xff09;中&#xff0c;合理调控反应途径以生成所需产物是最重要的挑战之一。基于此&#xff0c;中国科学院化学研究所韩布兴院士和朱庆宫研究员等人报道了一系列稀土-铜混合相催化剂&#xff0c;通过调整催化剂的组成和结构&#xff…

Nodejs的安装以及配置(node-v12.16.1-x64.msi)

Nodejs的安装以及配置 1、安装 node-v12.16.1-x64.msi点击安装&#xff0c;注意以下步骤 本文设置nodejs的安装的路径&#xff1a;D:\soft\nodejs 继续点击next&#xff0c;选中Add to PATH &#xff0c;旁边的英文告诉我们会把 环境变量 给我们配置好 当然也可以只选择 Nod…

工业自动化工厂PLC远程控制网关物联网应用

远程控制网关在工厂自动化领域中起到了至关重要的作用&#xff0c;特别是在工厂PLC数据通讯方面。它充当着数据传输的桥梁&#xff0c;连接了工厂中的各类设备和系统&#xff0c;实现了远程监控和控制的功能。本文将详细介绍远程控制网关在工厂PLC数据通讯中的应用。 远程控制网…

Hadoop知识点全面总结

文章目录 什么是HadoopHadoop发行版介绍Hadoop版本演变历史Hadoop3.x的细节优化Hadoop三大核心组件介绍HDFS体系结构NameNode介绍总结 SecondaryNameNode介绍DataNode介绍DataNode总结 MapReduce介绍分布式计算介绍MapReduce原理剖析MapReduce之Map阶段MapReduce之Reduce阶段 实…

Langchain-Chatchat-win10本地安装部署成功笔记(CPU)

Langchain-Chatchat&#xff08;原Langchain-ChatGLM&#xff09;基于 Langchain 与 ChatGLM 等语言模型的本地知识库问答 | Langchain-Chatchat (formerly langchain-ChatGLM), local knowledge based LLM (like ChatGLM) QA app with langchain。 开源网址&#xff1a;https:…

leetcode周赛 第 370 场周赛

2923. 找到冠军 I 一场比赛中共有 n 支队伍&#xff0c;按从 0 到 n - 1 编号。 给你一个下标从 0 开始、大小为 n * n 的二维布尔矩阵 grid 。对于满足 0 < i, j < n - 1 且 i ! j 的所有 i, j &#xff1a;如果 grid[i][j] 1&#xff0c;那么 i 队比 j 队 强 &…

第五章:java构造方法与对象创建

系列文章目录 文章目录 系列文章目录前言一、构造方法&#xff08;构造器&#xff09;二、对象创建流程总结 前言 构造方法由程序自动调用&#xff0c;完成对象初始化。 一、构造方法&#xff08;构造器&#xff09; 构造方法又叫构造器(constructor)&#xff0c; 是类的一种…

将字符串转换为日期型对象date.fromisoformat(str)

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 将字符串转换为日期型对象 date.fromisoformat(str) 选择题 下列代码执行后&#xff0c;变量d的数据类型是? s 2023-11-01 d date.fromisoformat(s) print(f"【显示】s {s}") p…

大数据毕业设计选题推荐-家具公司运营数据分析平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

SLAM从入门到精通(车道线检测)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 对于slam而言&#xff0c;大家一般想到的就是去通过传感器找特征点&#xff0c;进而借助于特征点去定位机器人的位置。但是对于用户或者厂家来说&a…

Linux常用命令——cd命令

在线Linux命令查询工具 cd 切换用户当前工作目录 补充说明 cd命令用来切换工作目录至dirname。 其中dirName表示法可为绝对路径或相对路径。若目录名称省略&#xff0c;则变换至使用者的home directory(也就是刚login时所在的目录)。另外&#xff0c;~也表示为home directo…

LabVIEW开发多速率实时混合仿真

LabVIEW开发多速率实时混合仿真 混合仿真是一种子结构技术&#xff0c;通过将数值建模的优点与实验测试的优点相结合来模拟感兴趣的结构。模拟结构的其余部分特别令人感兴趣&#xff0c;因此可以进行物理复制&#xff0c;以揭示粘弹性、屈曲、速率相关特性或其他非线性效应的影…

微信小程序文件上传wx.uploadFile

网页版查看了一下负载要求是这样 wx.uploadFile({url: ${wx.getStorageSync(apiUrl)}//sysFileInfo/upload?token${wx.getStorageSync(token)}, // 仅为示例&#xff0c;非真实的接口地址filePath: files[0].url,name: file,formData: {secretFlag: Y },success: (res) > {…

Android 11.0 framework层实现app默认全屏显示

1.前言 在11.0的系统rom定制化开发中,在对于第三方app全屏显示的功能需求开发中,需要默认app全屏显示,针对这一个要求,就需要在系统启动app 的过程中,在绘制app阶段就设置全屏属性,接下来就实现这个功能 效果图如下: 2.framework层实现app默认全屏显示的核心类 framewo…

数据跨领域应用实例—公路数据应用场景(一)

2023年10月25日&#xff0c;国家数据局正式揭牌。标志着我国数据基础制度正在不断完善&#xff0c;数据资源使用水平稳额步提升&#xff0c;数据要素市场将进入发展快车道。当前&#xff0c;数字经济已成为我国经济高质量发展的新动能&#xff0c;国家数据局的成立&#xff0c;…

数量关系(蒙题技巧)

选中间2项 选三的倍数 数学中很少无法确定 难题 质数很难消掉