learn.lianglianglee.com/专栏/MySQL实战45讲/21 为什么我只改一行的语句,锁这么多?.md.html
2022-08-14 03:40:33 +08:00

454 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>21 为什么我只改一行的语句,锁这么多?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/MySQL实战45讲/00 开篇词 这一次让我们一起来搞懂MySQL.md.html">00 开篇词 这一次让我们一起来搞懂MySQL</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/01 基础架构一条SQL查询语句是如何执行的.md.html">01 基础架构一条SQL查询语句是如何执行的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/02 日志系统一条SQL更新语句是如何执行的.md.html">02 日志系统一条SQL更新语句是如何执行的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/03 事务隔离:为什么你改了我还看不见?.md.html">03 事务隔离:为什么你改了我还看不见?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/04 深入浅出索引(上).md.html">04 深入浅出索引(上)</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/05 深入浅出索引(下).md.html">05 深入浅出索引(下)</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/06 全局锁和表锁 :给表加个字段怎么有这么多阻碍?.md.html">06 全局锁和表锁 :给表加个字段怎么有这么多阻碍?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/07 行锁功过:怎么减少行锁对性能的影响?.md.html">07 行锁功过:怎么减少行锁对性能的影响?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/08 事务到底是隔离的还是不隔离的?.md.html">08 事务到底是隔离的还是不隔离的?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/09 普通索引和唯一索引,应该怎么选择?.md.html">09 普通索引和唯一索引,应该怎么选择?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/10 MySQL为什么有时候会选错索引.md.html">10 MySQL为什么有时候会选错索引</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/11 怎么给字符串字段加索引?.md.html">11 怎么给字符串字段加索引?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/12 为什么我的MySQL会“抖”一下.md.html">12 为什么我的MySQL会“抖”一下</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/13 为什么表数据删掉一半,表文件大小不变?.md.html">13 为什么表数据删掉一半,表文件大小不变?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/14 count()这么慢,我该怎么办?.md.html">14 count()这么慢,我该怎么办?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/15 答疑文章(一):日志和索引相关问题.md.html">15 答疑文章(一):日志和索引相关问题</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/16 “order by”是怎么工作的.md.html">16 “order by”是怎么工作的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/17 如何正确地显示随机消息?.md.html">17 如何正确地显示随机消息?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/18 为什么这些SQL语句逻辑相同性能却差异巨大.md.html">18 为什么这些SQL语句逻辑相同性能却差异巨大</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/19 为什么我只查一行的语句,也执行这么慢?.md.html">19 为什么我只查一行的语句,也执行这么慢?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/20 幻读是什么,幻读有什么问题?.md.html">20 幻读是什么,幻读有什么问题?</a>
</li>
<li>
<a class="current-tab" href="/专栏/MySQL实战45讲/21 为什么我只改一行的语句,锁这么多?.md.html">21 为什么我只改一行的语句,锁这么多?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/22 MySQL有哪些“饮鸩止渴”提高性能的方法.md.html">22 MySQL有哪些“饮鸩止渴”提高性能的方法</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/23 MySQL是怎么保证数据不丢的.md.html">23 MySQL是怎么保证数据不丢的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/24 MySQL是怎么保证主备一致的.md.html">24 MySQL是怎么保证主备一致的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/25 MySQL是怎么保证高可用的.md.html">25 MySQL是怎么保证高可用的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/26 备库为什么会延迟好几个小时?.md.html">26 备库为什么会延迟好几个小时?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/27 主库出问题了,从库怎么办?.md.html">27 主库出问题了,从库怎么办?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/28 读写分离有哪些坑?.md.html">28 读写分离有哪些坑?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/29 如何判断一个数据库是不是出问题了?.md.html">29 如何判断一个数据库是不是出问题了?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/30 答疑文章(二):用动态的观点看加锁.md.html">30 答疑文章(二):用动态的观点看加锁</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/31 误删数据后除了跑路,还能怎么办?.md.html">31 误删数据后除了跑路,还能怎么办?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/32 为什么还有kill不掉的语句.md.html">32 为什么还有kill不掉的语句</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/33 我查这么多数据,会不会把数据库内存打爆?.md.html">33 我查这么多数据,会不会把数据库内存打爆?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/34 到底可不可以使用join.md.html">34 到底可不可以使用join</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/35 join语句怎么优化.md.html">35 join语句怎么优化</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/36 为什么临时表可以重名?.md.html">36 为什么临时表可以重名?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/37 什么时候会使用内部临时表?.md.html">37 什么时候会使用内部临时表?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/38 都说InnoDB好那还要不要使用Memory引擎.md.html">38 都说InnoDB好那还要不要使用Memory引擎</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/39 自增主键为什么不是连续的?.md.html">39 自增主键为什么不是连续的?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/40 insert语句的锁为什么这么多.md.html">40 insert语句的锁为什么这么多</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/41 怎么最快地复制一张表?.md.html">41 怎么最快地复制一张表?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/42 grant之后要跟着flush privileges吗.md.html">42 grant之后要跟着flush privileges吗</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/43 要不要使用分区表?.md.html">43 要不要使用分区表?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/44 答疑文章(三):说一说这些好问题.md.html">44 答疑文章(三):说一说这些好问题</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/45 自增id用完怎么办.md.html">45 自增id用完怎么办</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/我的MySQL心路历程.md.html">我的MySQL心路历程</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/结束语 点线网面一起构建MySQL知识网络.md.html">结束语 点线网面一起构建MySQL知识网络</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>21 为什么我只改一行的语句,锁这么多?</h1>
<p>在上一篇文章中,我和你介绍了间隙锁和 next-key lock 的概念,但是并没有说明加锁规则。间隙锁的概念理解起来确实有点儿难,尤其在配合上行锁以后,很容易在判断是否会出现锁等待的问题上犯错。</p>
<p>所以今天,我们就先从这个加锁规则开始吧。</p>
<p>首先说明一下,这些加锁规则我没在别的地方看到过有类似的总结,以前我自己判断的时候都是想着代码里面的实现来脑补的。这次为了总结成不看代码的同学也能理解的规则,是我又重新刷了代码临时总结出来的。所以,<strong>这个规则有以下两条前提说明:</strong></p>
<ol>
<li>MySQL 后面的版本可能会改变加锁策略,所以这个规则只限于截止到现在的最新版本,即 5.x 系列 &lt;=5.7.248.0 系列 &lt;=8.0.13。</li>
<li>如果大家在验证中有发现 bad case 的话,请提出来,我会再补充进这篇文章,使得一起学习本专栏的所有同学都能受益。</li>
</ol>
<p>因为间隙锁在可重复读隔离级别下才有效,所以本篇文章接下来的描述,若没有特殊说明,默认是可重复读隔离级别。</p>
<p><strong>我总结的加锁规则里面包含了两个“原则”、两个“优化”和一个“bug”。</strong></p>
<ol>
<li>原则 1加锁的基本单位是 next-key lock。希望你还记得next-key lock 是前开后闭区间。</li>
<li>原则 2查找过程中访问到的对象才会加锁。</li>
<li>优化 1索引上的等值查询给唯一索引加锁的时候next-key lock 退化为行锁。</li>
<li>优化 2索引上的等值查询向右遍历时且最后一个值不满足等值条件的时候next-key lock 退化为间隙锁。</li>
<li>一个 bug唯一索引上的范围查询会访问到不满足条件的第一个值为止。</li>
</ol>
<p>我还是以上篇文章的表 t 为例,和你解释一下这些规则。表 t 的建表语句和初始化语句如下。</p>
<pre><code>CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
</code></pre>
<p>接下来的例子基本都是配合着图片说明的,所以我建议你可以对照着文稿看,有些例子可能会“毁三观”,也建议你读完文章后亲手实践一下。</p>
<h1>案例一:等值查询间隙锁</h1>
<p>第一个例子是关于等值条件操作间隙:</p>
<p><img src="assets/585dfa8d0dd71171a6fa16bed4ba816c.png" alt="img" /></p>
<p>图 1 等值查询的间隙锁</p>
<p>由于表 t 中没有 id=7 的记录,所以用我们上面提到的加锁规则判断一下的话:</p>
<ol>
<li>根据原则 1加锁单位是 next-key locksession A 加锁范围就是 (5,10]</li>
<li>同时根据优化 2这是一个等值查询 (id=7),而 id=10 不满足查询条件next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)。</li>
</ol>
<p>所以session B 要往这个间隙里面插入 id=8 的记录会被锁住,但是 session C 修改 id=10 这行是可以的。</p>
<h1>案例二:非唯一索引等值锁</h1>
<p>第二个例子是关于覆盖索引上的锁:</p>
<p><img src="assets/465990fe8f6b418ca3f9992bd1bb5465.png" alt="img" /></p>
<p>图 2 只加在非唯一索引上的锁</p>
<p>看到这个例子,你是不是有一种“该锁的不锁,不该锁的乱锁”的感觉?我们来分析一下吧。</p>
<p>这里 session A 要给索引 c 上 c=5 的这一行加上读锁。</p>
<ol>
<li>根据原则 1加锁单位是 next-key lock因此会给 (0,5] 加上 next-key lock。</li>
<li>要注意 c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的,需要向右遍历,查到 c=10 才放弃。根据原则 2访问到的都要加锁因此要给 (5,10] 加 next-key lock。</li>
<li>但是同时这个符合优化 2等值判断向右遍历最后一个值不满足 c=5 这个等值条件,因此退化成间隙锁 (5,10)。</li>
<li>根据原则 2 <strong>只有访问到的对象才会加锁</strong>,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成。</li>
</ol>
<p>但 session C 要插入一个 (7,7,7) 的记录,就会被 session A 的间隙锁 (5,10) 锁住。</p>
<p>需要注意在这个例子中lock in share mode 只锁覆盖索引,但是如果是 for update 就不一样了。 执行 for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。</p>
<p>这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用 lock in share mode 来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如,将 session A 的查询语句改成 select d from t where c=5 lock in share mode。你可以自己验证一下效果。</p>
<h1>案例三:主键索引范围锁</h1>
<p>第三个例子是关于范围查询的。</p>
<p>举例之前,你可以先思考一下这个问题:对于我们这个表 t下面这两条查询语句加锁范围相同吗</p>
<pre><code>mysql&gt; select * from t where id=10 for update;
mysql&gt; select * from t where id&gt;=10 and id&lt;11 for update;
</code></pre>
<p>你可能会想id 定义为 int 类型,这两个语句就是等价的吧?其实,它们并不完全等价。</p>
<p>在逻辑上,这两条查语句肯定是等价的,但是它们的加锁规则不太一样。现在,我们就让 session A 执行第二个查询语句,来看看加锁效果。</p>
<p><img src="assets/30b839bf941f109b04f1a36c302aea80.png" alt="img" /></p>
<p>图 3 主键索引上范围查询的锁</p>
<p>现在我们就用前面提到的加锁规则,来分析一下 session A 会加什么锁呢?</p>
<ol>
<li>开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10]。 根据优化 1 主键 id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。</li>
<li>范围查找就往后继续找,找到 id=15 这一行停下来,因此需要加 next-key lock(10,15]。</li>
</ol>
<p>所以session A 这时候锁的范围就是主键索引上,行锁 id=10 和 next-key lock(10,15]。这样session B 和 session C 的结果你就能理解了。</p>
<p>这里你需要注意一点,首次 session A 定位查找 id=10 的行的时候,是当做等值查询来判断的,而向右扫描到 id=15 的时候,用的是范围查询判断。</p>
<h1>案例四:非唯一索引范围锁</h1>
<p>接下来,我们再看两个范围查询加锁的例子,你可以对照着案例三来看。</p>
<p>需要注意的是,与案例三不同的是,案例四中查询语句的 where 部分用的是字段 c。</p>
<p><img src="assets/7381475e9e951628c9fc907f5a57697a.png" alt="img" /></p>
<p>图 4 非唯一索引范围锁</p>
<p>这次 session A 用字段 c 来判断,加锁规则跟案例三唯一的不同是:在第一次用 c=10 定位记录的时候,索引 c 上加了 (5,10] 这个 next-key lock 后,由于索引 c 是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终 sesion A 加的锁是,索引 c 上的 (5,10] 和 (10,15] 这两个 next-key lock。</p>
<p>所以从结果上来看sesson B 要插入8,8,8) 的这个 insert 语句时就被堵住了。</p>
<p>这里需要扫描到 c=15 才停止扫描,是合理的,因为 InnoDB 要扫到 c=15才知道不需要继续往后找了。</p>
<h1>案例五:唯一索引范围锁 bug</h1>
<p>前面的四个案例,我们已经用到了加锁规则中的两个原则和两个优化,接下来再看一个关于加锁规则中 bug 的案例。</p>
<p><img src="assets/b105f8c4633e8d3a84e6422b1b1a316d.png" alt="img" /></p>
<p>图 5 唯一索引范围锁的 bug</p>
<p>session A 是一个范围查询,按照原则 1 的话,应该是索引 id 上只加 (10,15] 这个 next-key lock并且因为 id 是唯一键,所以循环判断到 id=15 这一行就应该停止了。</p>
<p>但是实现上InnoDB 会往前扫描到第一个不满足条件的行为止,也就是 id=20。而且由于这是个范围扫描因此索引 id 上的 (15,20] 这个 next-key lock 也会被锁上。</p>
<p>所以你看到了session B 要更新 id=20 这一行是会被锁住的。同样地session C 要插入 id=16 的一行,也会被锁住。</p>
<p>照理说,这里锁住 id=20 这一行的行为,其实是没有必要的。因为扫描到 id=15就可以确定不用往后再找了。但实现上还是这么做了因此我认为这是个 bug。</p>
<p>我也曾找社区的专家讨论过,官方 bug 系统上也有提到,但是并未被 verified。所以认为这是 bug 这个事儿,也只能算我的一家之言,如果你有其他见解的话,也欢迎你提出来。</p>
<h1>案例六:非唯一索引上存在&quot;等值&quot;的例子</h1>
<p>接下来的例子,是为了更好地说明“间隙”这个概念。这里,我给表 t 插入一条新记录。</p>
<pre><code>mysql&gt; insert into t values(30,10,30);
</code></pre>
<p>新插入的这一行 c=10也就是说现在表里有两个 c=10 的行。那么,这时候索引 c 上的间隙是什么状态了呢?你要知道,由于非唯一索引上包含主键的值,所以是不可能存在“相同”的两行的。</p>
<p><img src="assets/c1fda36c1502606eb5be3908011ba159.png" alt="img" /></p>
<p>图 6 非唯一索引等值的例子</p>
<p>可以看到,虽然有两个 c=10但是它们的主键值 id 是不同的(分别是 10 和 30因此这两个 c=10 的记录之间,也是有间隙的。</p>
<p>图中我画出了索引 c 上的主键 id。为了跟间隙锁的开区间形式进行区别我用 (c=10,id=30) 这样的形式,来表示索引上的一行。</p>
<p>现在,我们来看一下案例六。</p>
<p>这次我们用 delete 语句来验证。注意delete 语句加锁的逻辑,其实跟 select ... for update 是类似的也就是我在文章开始总结的两个“原则”、两个“优化”和一个“bug”。</p>
<p><img src="assets/b55fb0a1cac3500b60e1cf9779d2da78.png" alt="img" /></p>
<p>图 7 delete 示例</p>
<p>这时session A 在遍历的时候,先访问第一个 c=10 的记录。同样地,根据原则 1这里加的是 (c=5,id=5) 到 (c=10,id=10) 这个 next-key lock。</p>
<p>然后session A 向右查找,直到碰到 (c=15,id=15) 这一行,循环才结束。根据优化 2这是一个等值查询向右查找到了不满足条件的行所以会退化成 (c=10,id=10) 到 (c=15,id=15) 的间隙锁。</p>
<p>也就是说,这个 delete 语句在索引 c 上的加锁范围,就是下图中蓝色区域覆盖的部分。
<img src="assets/bb0ad92483d71f0dcaeeef278f89cb24.png" alt="img" /></p>
<p>图 8 delete 加锁效果示例</p>
<p>这个蓝色区域左右两边都是虚线,表示开区间,即 (c=5,id=5) 和 (c=15,id=15) 这两行上都没有锁。</p>
<h1>案例七limit 语句加锁</h1>
<p>例子 6 也有一个对照案例,场景如下所示:</p>
<p><img src="assets/afc3a08ae7a254b3251e41b2a6dae02e.png" alt="img" /></p>
<p>图 9 limit 语句加锁</p>
<p>这个例子里session A 的 delete 语句加了 limit 2。你知道表 t 里 c=10 的记录其实只有两条,因此加不加 limit 2删除的效果都是一样的但是加锁的效果却不同。可以看到session B 的 insert 语句执行通过了,跟案例六的结果不同。</p>
<p>这是因为,案例七里的 delete 语句明确加了 limit 2 的限制,因此在遍历到 (c=10, id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了。</p>
<p>因此,索引 c 上的加锁范围就变成了从c=5,id=5) 到c=10,id=30) 这个前开后闭区间,如下图所示:
<img src="assets/e5408ed94b3d44985073255db63bd0d5.png" alt="img" /></p>
<p>图 10 带 limit 2 的加锁效果</p>
<p>可以看到,(c=10,id=30之后的这个间隙并没有在加锁范围里因此 insert 语句插入 c=12 是可以执行成功的。</p>
<p>这个例子对我们实践的指导意义就是,<strong>在删除数据的时候尽量加 limit</strong>。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。</p>
<h1>案例八:一个死锁的例子</h1>
<p>前面的例子中,我们在分析的时候,是按照 next-key lock 的逻辑来分析的因为这样分析比较方便。最后我们再看一个案例目的是说明next-key lock 实际上是间隙锁和行锁加起来的结果。</p>
<p>你一定会疑惑,这个概念不是一开始就说了吗?不要着急,我们先来看下面这个例子:</p>
<p><img src="assets/7b911a4c995706e8aa2dd96ff0f36506.png" alt="img" /></p>
<p>图 11 案例八的操作序列</p>
<p>现在,我们按时间顺序来分析一下为什么是这样的结果。</p>
<ol>
<li>session A 启动事务后执行查询语句加 lock in share mode在索引 c 上加了 next-key lock(5,10] 和间隙锁 (10,15)</li>
<li>session B 的 update 语句也要在索引 c 上加 next-key lock(5,10] ,进入锁等待;</li>
<li>然后 session A 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死锁InnoDB 让 session B 回滚。</li>
</ol>
<p>你可能会问session B 的 next-key lock 不是还没申请成功吗?</p>
<p>其实是这样的session B 的“加 next-key lock(5,10] ”操作,实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;然后加 c=10 的行锁,这时候才被锁住的。</p>
<p>也就是说,我们在分析加锁规则的时候可以用 next-key lock 来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的。</p>
<h1>小结</h1>
<p>这里我再次说明一下,我们上面的所有案例都是在可重复读隔离级别 (repeatable-read) 下验证的。同时,可重复读隔离级别遵守两阶段锁协议,所有加锁的资源,都是在事务提交或者回滚的时候才释放的。</p>
<p>在最后的案例中,你可以清楚地知道 next-key lock 实际上是由间隙锁加行锁实现的。如果切换到读提交隔离级别 (read-committed) 的话,就好理解了,过程中去掉间隙锁的部分,也就是只剩下行锁的部分。</p>
<p>其实读提交隔离级别在外键场景下还是有间隙锁,相对比较复杂,我们今天先不展开。</p>
<p>另外,在读提交隔离级别下还有一个优化,即:语句执行过程中加上的行锁,在语句执行完成后,就要把“不满足条件的行”上的行锁直接释放了,不需要等到事务提交。</p>
<p>也就是说,读提交隔离级别下,锁的范围更小,锁的时间更短,这也是不少业务都默认使用读提交隔离级别的原因。</p>
<p>不过,我希望你学过今天的课程以后,可以对 next-key lock 的概念有更清晰的认识,并且会用加锁规则去判断语句的加锁范围。</p>
<p>在业务需要使用可重复读隔离级别的时候,能够更细致地设计操作数据库的语句,解决幻读问题的同时,最大限度地提升系统并行处理事务的能力。</p>
<p>经过这篇文章的介绍,你再看一下上一篇文章最后的思考题,再来尝试分析一次。</p>
<p>我把题目重新描述和简化一下:还是我们在文章开头初始化的表 t里面有 6 条记录,图 12 的语句序列中,为什么 session B 的 insert 操作,会被锁住呢?
<img src="assets/3a7578e104612a188a2d574eaa3bd81e.png" alt="img" /></p>
<p>图 12 锁分析思考题</p>
<p>另外,如果你有兴趣多做一些实验的话,可以设计好语句序列,在执行之前先自己分析一下,然后实际地验证结果是否跟你的分析一致。</p>
<p>对于那些你自己无法解释的结果,可以发到评论区里,后面我争取挑一些有趣的案例在文章中分析。</p>
<p>你可以把你关于思考题的分析写在留言区,也可以分享你自己设计的锁验证方案,我会在下一篇文章的末尾选取有趣的评论跟大家分享。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。</p>
<h2>上期问题时间</h2>
<p>上期的问题,我在本期继续作为了课后思考题,所以会在下篇文章再一起公布“答案”。</p>
<p>这里,我展开回答一下评论区几位同学的问题。</p>
<ul>
<li>@令狐少侠 说,以前一直认为间隙锁只在二级索引上有。现在你知道了,有间隙的地方就可能有间隙锁。</li>
<li>@浪里白条 同学问,如果是 varchar 类型,加锁规则是什么样的。
回答实际上在判断间隙的时候varchar 和 int 是一样的,排好序以后,相邻两个值之间就有间隙。</li>
<li>有几位同学提到说,上一篇文章自己验证的结果跟案例一不同,就是在 session A 执行完这两个语句:</li>
</ul>
<pre><code>begin;
select * from t where d=5 for update; /*Q1*/
</code></pre>
<p>以后session B 的 update 和 session C 的 insert 都会被堵住。这是不是跟文章的结论矛盾?</p>
<p>其实不是的,这个例子用的是反证假设,就是假设不堵住,会出现问题;然后,推导出 session A 需要锁整个表所有的行和所有间隙。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/MySQL实战45讲/20 幻读是什么,幻读有什么问题?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/MySQL实战45讲/22 MySQL有哪些“饮鸩止渴”提高性能的方法.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709972b6ff083d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>