REJAVA / LEARNING NOTES
MySQL(关系型数据库)锁范围与等待处理
用办件审批例子讲清记录锁、间隙锁、临键锁,区分锁等待与死锁及报错后的处理。
根据课程与真实学习交流整理;不是逐字稿
本次更新说明
- 整理为公开学习笔记,保留练习与隔日复测。
- 附课后问答:临键锁范围、死锁检测与超时、整事务重试、持锁等待用户。
热身 · 一 谁在等谁 · 二 记录锁 · 三 为什么插入也等 · 四 查找与加锁 · 五 等待与死锁 · 六 排查与收尾 · 七 应用 · 检验 · 表达 · 追问 · 隔日复测
学完能做到什么,为什么现在学
学完后,你能用完整的话说明:哪个事务想修改哪条办件,为什么需要等待另一个事务;也能解释新增记录为什么可能等待,以及死锁后为什么不能只重跑最后一条SQL。
本课仍用办件审批做例子。先看A、B先后修改同一条办件会发生什么,再认识锁的名称,最后处理等待与死锁。示例用于练习解释并发问题,表名和数据都是教学设定。
前置: 知道同一事务中的提交/回滚、主键和索引、普通读与锁定读的区别。 必学: 第一至七节、六道选择题和四问表达。第三节后可暂停。意向锁、插入意向锁、监控字段及双连接实验为拓展;不要求背所有兼容矩阵或凭SQL猜复杂执行计划。
主读: 官方 InnoDB(事务存储引擎) Locking(锁机制)。先读 Record Locks(记录锁)、Gap Locks(间隙锁)、Next-Key Locks(临键锁)三段,再看共享/排他锁。带着本课10、20、30的例子读,不先背监控输出。配读官方错误处理章节[5]。
学完正文后,可以对照本课末尾的课后问答,重点看死锁检测与锁等待超时的区别,以及怎样避免持锁等待用户确认。
热身:回忆上一课,三分钟即可
这部分用于回忆上一课,可以稍后再做。若现在主要想看懂新课,可以直接从第一节开始。
第12课:A在同一个读已提交事务里先普通查到9。B改成13并提交;A自己不写也未提交,再普通查,返回什么?
13。READ COMMITTED(读已提交)每次普通一致性读使用新视图,不要求A先提交。若同一事务使用可重复读并复用第一次读的视图,则仍读9。
第12课:旧视图上边界110,另一个事务编号恰好110,后来提交。能仅凭“已经提交”认为旧视图可见吗?
不能。编号大于或等于上边界的其他事务版本不可见,旧视图不因别人后来提交而改写。活跃名单说的也是建立时状态。
第12课:旧视图仍要读9,而当前行已改成13。为什么长时间不结束只读事务也可能有代价?
需要保留还原9所需的信息,可能拖住相关历史清理。查询完成后及时结束事务,不要等用户操作时一直保留旧事务。见第12课课后问答。
一、A还没办完,B为什么不能修改同一条记录
“数据库说B正在等锁,到底是谁在等什么?”
先看表里的实际数据。表名是 case_task,一行代表一件待处理的办件。下面的10、20、30都是 办件编号 ,不是事务编号,也不是人员编号。
| 办件编号 id | 审批状态 status | 经办人编号 owner_id |
|---|---|---|
| 10 | 待审批 | 7 |
| 20 | 待审批 | 7 |
| 30 | 待审批 | 7 |
id 是主键,因此两条办件不能使用同一个编号。表中目前只有主键索引。上面的数据已经提交,其他事务可以正常查询。
A、B代表两个数据库连接,可以理解为两个可以分别执行SQL的窗口。A在自己的连接里开始事务,B也在自己的连接里开始另一个事务。A提交自己的事务,不会替B提交。
下面先使用 REPEATABLE READ(可重复读)。除特别说明外,各个例子分别从上面的数据开始;暂不考虑外键、触发器、其他连接或表结构修改。普通等待例子假定尚未超时。把这些环境条件放在这里,是为了让我们先看清一个原因:两个事务正在争用同一条记录。
先把“拿锁”和“放锁”对应到具体动作
业务要求:20号办件只能审批成功一次。A准备审批它,先执行 SELECT status FROM case_task WHERE id = 20 FOR UPDATE;。这条语句的意思是:找到20号办件,读取它的状态,同时取得这条记录的更新锁。取得锁之后,其他事务如果也想修改20号办件,就需要等待。[7]
这条查询即使已经把“待审批”返回给A, A也仍然持有锁 。原因是A的事务还没有结束;查询语句执行完毕,与整个事务结束,是两个时点。
| 顺序 | A的操作 | B的操作及结果 |
|---|---|---|
| ① A先取得锁 | A开始事务,执行上面的锁定查询,读到“待审批” | B还没有开始审批 |
| ② B也来审批 | A的事务还没有提交,因此仍持有20号记录的锁 | B开始自己的事务,执行同一条锁定查询;这条查询需要的锁暂时拿不到,于是等待 |
| ③ A完成审批 | A把20号状态改成“已通过”,然后提交事务;数据库保存修改并释放A的锁 | B原来等待的查询现在可以继续,取得锁并读到“已通过” |
| ④ B处理重复请求 | A的事务已经结束 | B看到已经通过,就不再审批;B结束自己的事务,再向调用方返回“已处理” |
因此,“B等A”是下面这句话的简写: B的这条SQL需要取得20号记录的锁,但A还持有不允许同时存在的锁,所以B要等A释放锁。 数据库负责让冲突的操作等待;取得锁后能不能审批,仍由业务代码检查状态来决定。
如果B没有修改任何数据,B可以提交或回滚来结束这次事务。只在代码里执行 return(退出方法语句),并不能单独证明数据库事务已经结束;如果使用框架管理事务,还要确认框架已经完成收尾。
若B执行的是不带 FOR UPDATE 的普通查询,情况不同:它可以按照第12课的版本规则读取数据,但这次普通查询不会为之后的审批保留锁。B看到“待审批”,不代表其他人已经被禁止修改这条办件。
先试着自己解释,再看参考回答
A先通过锁定查询取得20号办件的锁。A的事务结束前,B对同一记录的更新或冲突的锁定查询需要等待。A提交后,B取得锁,再检查当前状态;如果已经审批,B就不再修改。查询返回结果不等于事务已经结束。
二、记录锁:A锁住20号,并不等于其他办件都不能改
“这把锁具体保护什么?为什么别人改30号不用等?”
继续看A刚才的查询:WHERE id = 20 指定一个确定的办件编号。数据库利用主键索引找到20号记录,然后给这条索引记录加锁。这样的锁叫 record lock(记录锁)。本例中,主键索引里的20号记录就是我们要保护的那条办件。[1]
假设A仍然持有20号记录的更新锁。让B分别尝试下面两件事:把20号办件的经办人改成9;或者把30号办件的经办人改成9。修改20号需要取得20号记录的锁,所以B要等A。修改30号需要的是30号记录的锁,A的20号锁不会阻止这个操作;本例没有其他冲突,所以B可以修改30号。
为什么有些锁能同时持有,有些不能
刚才用于更新保护的锁叫 exclusive lock(排他锁),常写成X。“排他”在这里表示:A已经持有某条记录的X锁时,其他事务不能同时取得这条记录上的S锁或X锁。B可以提出申请,但要等待。
另一种锁叫 shared lock(共享锁),常写成S。例如A执行 SELECT status FROM case_task WHERE id = 20 FOR SHARE;,就是读20号状态并申请S锁。B也申请20号的S锁时,两把S锁可以同时存在;但B要修改20号,需要X锁,就必须等A释放S锁。
| A已经取得的锁 | B现在准备做什么 | B能否立即取得所需的锁 |
|---|---|---|
| 20号记录的S锁 | B也对20号执行FOR SHARE | 可以。两个事务的S锁能够同时存在 |
| 20号记录的S锁 | B要修改20号,需要X锁 | 不能。B必须等待A释放S锁 |
| 20号记录的X锁 | B对20号申请S锁或X锁 | 不能。B必须等待A释放X锁 |
表里说的“可以同时存在”,教材通常叫 兼容 ;说“不能同时存在,需要等待”,通常叫 冲突 。这些词描述的是两笔事务所申请的锁之间的关系,不是说SQL语法写错了。
普通查询不在这张申请锁的表里。 在本课的读已提交和可重复读下,普通一致性读不因为读取20号就申请S锁。它仍可按照读视图读取可见版本,因此不能把X锁理解成“连任何普通查询都不允许执行”。
把一句常见说法拆开
“完整唯一键等值命中已有记录”说的就是:索引能保证编号唯一;条件像 id = 20 一样指定准确编号;表里确实有20号。这里主键只有id一列,条件已经给全。满足这些条件时,A的锁定查询只需对找到的记录加记录锁,不需要锁住它前面的空隙。下一节再看“空隙”是什么。[2]
拓展:为什么监控还可能显示表上的IX锁?
数据库还需要协调“锁整张表”和“锁表中某几条记录”这两类请求。因此A准备对表中的记录加X锁时,会先在表上取得IX(意向排他锁)。这个表级标记属于intention lock(意向锁)。A和B可以同时持有同一张表的IX;它不等于A独占整张表。是否需要等待,仍要继续看它们请求的具体记录锁。没有显式索引的表也会有数据库生成的隐藏聚簇索引供记录加锁使用。[1]
先试着自己解释,再看参考回答
记录锁加在索引记录上。A锁住20号记录时,B修改20号需要等待,但修改30号不必等待这把锁。S记录锁之间可以同时存在;X记录锁与别人的S或X记录锁不能同时存在。普通查询读取版本,不等于申请S锁。
三、间隙锁与临键锁:为什么新增15号也可能需要等待
“表里根本没有15号,数据库还能阻止别人插入它吗?”
先把表里的编号按大小排好:目前有10、20、30,没有11到19。现在B想新增15号办件。数据库会把这个新编号放到主键索引中10与20之间的位置。
| 已有记录 | 两条记录之间的位置 | 已有记录 |
|---|---|---|
| 10号 | 11、12、…、15、…、19目前都不存在 | 20号 |
这段可供新编号插入的位置,叫 间隙 。它是索引顺序上的范围,不是预先存好了九行空白数据,也不是磁盘里某块空闲空间。
先看哪种查询会需要保护这段范围
换一个任务:A准备读取并处理“编号大于10”的一批办件,希望本次事务处理期间,其他事务不能往正在保护的范围里新增办件。A先开始可重复读事务,然后执行 SELECT id FROM case_task WHERE id > 10 FOR UPDATE;。
这次查询会读到20和30。可是,如果数据库只给已有20、30加记录锁,B仍能新增15号;15也满足“编号大于10”,处理范围内就多了一条新记录。因此,这类锁定范围查询还需要阻止向相应间隙插入。[1][2]
本例会保护20前面的间隙,因此B插入15号需要等待A释放相关锁。 这条范围查询还会保护其他记录和范围。 下面只拿20及它前面的间隙解释一种锁的组成,不把这一小段当成整条SQL的全部锁。
只保护空隙:间隙锁
gap lock(间隙锁)的作用是阻止其他事务往某段间隙插入新记录。比如A只持有10与20之间的纯间隙锁,B新增15号就需要等待。但这把 纯间隙锁没有锁住20号记录本身 ,所以它不会阻止B只修改20号的经办人。这里假定B不修改办件编号,也没有其他锁挡住B。
把“大于10、小于20”写短一点,就是 (10,20)。左右圆括号都表示不包含端点:15在里面,10和20不在里面。
同时保护空隙与右边记录:临键锁
next-key lock(临键锁)把两部分放在一起:一部分保护20号记录,另一部分保护20号之前、10号之后的间隙。它可以写成 (10,20]。右边改用方括号,是因为这次还包含20号记录本身。
如果A取得的是这段范围的 X临键锁 ,B插入15号会因为间隙受到保护而等待;B修改20号则会因为20号记录上有X锁而等待。两个操作都等A,但被阻止的原因不同。
| 假设A持有的锁 | B要新增15号办件 | B要修改20号的经办人 |
|---|---|---|
| 只锁20号记录:X记录锁 | 可以执行。A没有保护15要插入的位置 | 需要等待。A锁住了B要修改的20号记录 |
| 只保护10与20之间:纯间隙锁 | 需要等待。15要插入的位置被A保护 | 可以执行。这把锁不保护20号记录本身 |
| 同时保护前面间隙与20:X临键锁 | 需要等待。原因是临键锁的间隙部分 | 需要等待。原因是临键锁的记录部分 |
这张表比较三个不同场景,不是让A同时取得三把锁。B的“新增”和“修改”也分别试想,不是先插入并卡住后又让同一个连接继续更新。表中“可以执行”只适用于我们给出的条件:没有其他锁或数据库约束导致冲突。
为什么“没有查到记录”不代表“没有锁”
A在可重复读事务中用锁定查询寻找15号,虽然结果为空,但数据库仍可以保护15所在的间隙,阻止其他事务在A结束前插入15。原因是要保护的对象还可以是插入位置,不一定是现成的一行。因此上一节“只需记录锁”的结论特别强调:必须实际找到了那条唯一记录。
以上讲的是带 FOR UPDATE 等保护的锁定读取。普通可重复读查询使用旧视图读取版本,不会仅仅因为是可重复读,就自动给查询范围加临键锁。
拓展:纯间隙锁为什么可以共存?插入意向锁又是什么?
假设A、B都要求“暂时不准往10与20之间插入新编号”,这两项要求可以同时存在,所以两把纯间隙锁不需要互相排斥。它们都可以阻止别的事务插入。不要把上一节S/X记录锁的冲突关系套到纯间隙锁上。
事务C想插入15时,会提出表示插入位置的insert intention lock(插入意向锁)请求。如果这个位置被A的间隙锁保护,C就要等待。如果没有这种保护,也没有其他冲突,分别插入15和16的两个事务不必仅因为处于同一个间隙就互相等待。插入意向锁与上一节的表级意向锁是不同概念。[1]
先试着自己解释,再看参考回答
间隙是索引中相邻记录之间可插入新记录的位置。间隙锁能阻止新增15,却不一定保护已有20。20上的临键锁把20记录与它前面的间隙一起保护,因此X临键锁既能挡住新增15,也能挡住修改20。
这里可以暂停。你可以回看上表,先把“新增一条记录”和“修改已有记录”的区别弄清楚,再往下读。
四、只查出一条结果,为什么还可能影响很多记录
“SQL最后只返回一行,数据库是否就只给这一行加了锁?”
要分清两个过程: 数据库为了找到答案,检查了哪些记录;检查完以后,又返回了哪些记录。 前者是搜索或扫描过程,后者是查询结果,两者数量可能不同。
举个独立例子:表里现在有一万条办件,只有一条状态是“待审批”,但状态列没有合适索引。A执行按状态筛选的锁定查询。数据库可能需要逐条检查很多办件,才能找到那一条待审批记录。在这个过程中,锁定查询可能给扫描经过的记录及相关间隙加锁,影响范围不一定只有最终返回的那一条。[2]
所以,排查时不能只说“结果就一行,锁一定很少”。应该先看执行计划,确认数据库利用哪一个索引、需要扫描什么范围;再看实际锁信息,确认当时哪些记录或间隙被保护。
按这个顺序问,就不容易漏掉原因
- 这条SQL要做什么? 普通查询、锁定查询和更新,使用锁的方式不同。
- 数据库怎样找到目标? 例如用
id = 20直接找主键,还是检查大量记录后按状态筛选。 - 事务使用什么隔离级别? 本课前面的间隙保护主要放在可重复读的锁定查询中讨论。
- 此刻实际有哪些锁? 最后再根据监控中的锁类型和索引记录,确定谁会被挡住;第六节会说明怎样读这类信息。
如果换成 READ COMMITTED(读已提交),普通搜索和索引扫描一般不使用间隙锁,但修改同一条记录时仍需要处理记录锁冲突。外键检查、重复键检查还有间隙锁的例外。换隔离级别会改变一些行为,却不等于所有等待和死锁都会消失。[1]
拓展:如果走的是二级索引呢?
二级索引是主键索引之外的索引,例如在状态列上建立的索引。数据库先沿状态索引找到目标,再访问对应的主键记录。若搜索过程需要对二级索引记录加排他锁,还可能同时给对应的聚簇索引记录加锁。因此监控中可能不止看到一个索引上的锁。复杂SQL的完整锁范围要结合实际执行路径核对。[2]
先试着自己解释,再看参考回答
返回一条记录,不代表数据库只检查过一条记录。锁定查询的加锁范围还与扫描过程、索引和隔离级别有关。我会先看执行计划,再核对当时的锁信息,不只根据返回行数猜锁范围。
五、一个人等另一个人,与两个人互相等,有什么区别
“请求一直没返回,怎样判断是普通等待还是死锁?”
先回到20号办件:A持有20号记录的X锁,B想修改这条记录,所以B的更新语句需要等待。A没有被B阻挡,它可以把自己的工作做完并提交。A释放锁后,B就有机会继续。这叫 lock wait(锁等待),但还不是死锁。
现在换成两条办件一起交接的任务:10号和20号必须在同一个事务里更换经办人,要么两条都成功,要么都撤销。A、B各自准备完成一次这样的交接,但操作顺序相反。A先改10号,B先改20号。
两个连接都使用可重复读,每次更新都用 WHERE id = 编号 找到那条已存在的记录,只修改经办人、不修改主键。更新成功后暂不提交,这样锁会留在各自事务中。按下面顺序执行:
| 步骤 | A发生了什么 | B发生了什么 |
|---|---|---|
| ① A先改10号 | A取得10号的X锁,完成修改,但事务还没提交 | B还没有修改记录 |
| ② B再改20号 | A仍然持有10号的锁 | B取得20号的X锁,完成修改,但事务还没提交 |
| ③ A尝试改20号 | 20号的锁还在B手里,A的第二条更新语句开始等待;A并没有释放10号的锁 | B仍然持有20号的锁 |
| ④ B尝试改10号 | A仍在等B释放20号的锁 | 10号的锁还在A手里,B的第二条更新语句也开始等待;B并没有释放20号的锁 |
A想继续: 需要B先释放20号记录的锁。
B想继续: 需要A先释放10号记录的锁。
两边的程序都打算在第二条更新成功后才提交,但现在第二条更新都被对方挡住了。
这就是 deadlock(死锁):A等B,B又等A,形成循环等待。这里“等A”或“等B”指等对方释放所需记录上的锁,不是等对方线程随便执行一下。[3]
步骤④由B连接发出,不是让已经卡在步骤③的A连接继续发下一条语句。例子假定步骤④发生时A还没超时,且数据库开启了死锁检测。
数据库怎样让这个循环结束
数据库检测到死锁后,会选择其中一个事务,把它从开始到现在的修改全部回滚,并释放它的锁。假设被回滚的是A:A之前对10号的修改被撤销,10号的锁被释放。B原来等待的更新就有机会继续,B完成后仍要由程序提交自己的事务。
数据库不保证每次都选A,也不保证总是选最后发出请求的事务。这里指定A,是为了把后面的过程讲清楚。
把操作顺序改成一致,为什么有帮助
让A、B都先改10号,再改20号。假设A先拿到10号的锁,B第一步修改10号时就会等待。此时B还没有执行修改20号的那一步,因此B没有拿着20号的锁。A可以继续修改20号并提交,然后B再继续。
这样就避免了本例“两边各拿着对方需要的一把锁”的局面。统一操作顺序、减少事务里的耗时工作、用合适索引减少扫描范围,都能降低死锁机会;但系统里还有其他操作路径,仍然需要处理死锁报错。[4]
先试着自己解释,再看参考回答
只有B等待A,而A能正常完成并释放锁,是普通等待。A持有10号的锁、等待B释放20号的锁,同时B又等待A释放10号的锁,才形成这个例子的死锁。数据库会回滚其中一个完整事务,让另一个事务有机会继续。
六、报错以后:先看哪些修改还在,再决定怎样处理
“最后一条更新报错了,只重跑这一条,为什么可能不对?”
情况一:数据库因为死锁回滚了A整个事务
继续上一节。A的目标是同时修改10号和20号。数据库回滚A整个事务时,A第一条已经执行成功的“修改10号”也被撤销了。 执行成功过,不代表最终已经提交。
如果A的程序现在只重新执行“修改20号”,就缺少了对10号的修改,两条一起交接的要求没有完成。因此,在业务允许再试一次的前提下,A应重新开始一个事务,重新检查两条办件当前是否允许交接,再完成两条修改,最后一起提交。这叫重试整个事务。[5]
为什么需要重新检查?因为A失败之后,B可能已经完成交接,记录状态已经变化。上一次得到的业务判断不能直接当作这一次的依据。程序还要限制重试次数、记录失败原因,反复失败时明确返回错误,不能无限循环。次数和间隔由具体业务决定。
情况二:A等锁太久,第二条更新超时
先把“超时”与“死锁”分开:在这个独立的超时例子里,B长时间不释放20号记录的锁,只是因为B的事务迟迟没结束;B没有在等待A。A已经修改10号但尚未提交,接着尝试修改20号。A的这条更新等到超过数据库设置的时限后,就会报 lock wait timeout(锁等待超时)。这个报错本身不证明发生了死锁。
使用默认的超时回滚配置时,数据库通常只撤销这条超时语句, A此前对10号的修改仍可能留在尚未提交的事务中 。由于业务要求两条一起成功,这次尝试应由应用明确执行整事务回滚,把10号的修改也撤销,再决定是否重新尝试。[5]
| A遇到什么错误 | 数据库已经撤销哪些修改 | 两条一起交接的业务接下来怎么做 |
|---|---|---|
| 死锁,A被选中回滚 | A整个事务的修改,包括先前对10号的修改 | 允许重试时,开启新事务,重新检查并修改两条记录 |
| 第二条更新等待超时,使用默认配置 | 通常只有超时的那条语句;前一步修改可能仍留在原事务中 | 先明确回滚原事务,不能提交只完成一半的交接 |
拓展:配置、框架和外部动作的边界
若开启 innodb_rollback_on_timeout,数据库对超时的回滚范围会不同;框架也可能在捕获异常后替你回滚整个事务。排查时要分别确认数据库做了什么、框架又做了什么,不能只记一条默认规则。
本课的重试示例只包含数据库修改。若程序已经发送短信,数据库回滚不能把短信收回来,因此不能不加区分地重复包含发送动作的全部流程。这类外部动作需要另外设计,不能当作普通SQL回滚。
没有死锁,只是一直等:先找谁还没有结束事务
再看一个常见流程:A开始事务,锁定读取20号办件,然后把页面显示给用户,却一直不结束事务,等用户点击确认。B此时要修改20号,就会等A释放锁。如果用户迟迟不点,B也会一直受影响,直到锁释放或等待超时。
要排查这件事,先确认B正在等哪条记录上的锁,以及这个锁由哪个事务持有;然后查看A为什么还没结束。知道“A正在等用户确认”,才能针对流程修改,而不是笼统地说“数据库慢”。
一种调整方式是:展示页面时先普通查询,等待用户操作期间不保持一个持有记录锁的事务;用户真正提交审批时,再开始一个短事务,锁定读取20号、重新检查状态、执行必要修改,随后提交或回滚。页面展示过的旧状态不能直接作为最终审批依据。
如果同一次审批要求修改两条记录,两条修改仍要放在同一个事务里。缩短事务是把等待用户等无须持锁的时间移出去,不是把本来必须一起成功的两条修改分开提交。
拓展:到哪里找“B在等A”的证据
performance_schema.data_lock_waits 记录当前哪些锁请求在等待、分别被哪些锁阻挡。结合 performance_schema.data_locks,可以找到锁对应的表、索引和类型。比如信息显示“B申请20号的X锁,阻挡者是A”,就能把B的慢请求与A持有的锁联系起来。[6]
一条B等待A的关系还不足以证明死锁,需要继续看A是否也在等待B,或是否形成更长的循环。最近一次已检测的死锁可以查看 SHOW ENGINE INNODB STATUS(查看引擎状态)。等待已经结束时,当前等待表可能没有旧关系,所以排查还要注意记录发生的时间。[4]
先试着自己解释,再看参考回答
先分清报错后哪些修改被撤销。死锁受害事务会整体回滚,允许重试时要从新事务开始重做两条修改。默认锁等待超时可能只撤销第二条语句,所以本例还要明确回滚整个事务。对一直等待的请求,先找持锁事务,再检查它为何迟迟不结束。
七、应用:先解释冲突,再提出处理办法
“换一组编号,能自己解释等待原因吗?”
A.判断三种操作是否需要等待
表里只有10、20、30号办件,办件编号是主键。A已经在自己的事务中取得 (20,30] 的X临键锁,并且还没有提交或回滚。也就是说,A同时保护着20与30之间的插入位置,以及30号记录本身。
B在另一个连接中准备执行操作。请把下面三项看成三个独立的小场景,分别判断:
- B新增25号办件,是否需要等待A?为什么?
- B把30号办件的经办人改成9,是否需要等待A?为什么?
- B把20号办件的经办人改成9,是否需要等待A?为什么?
本题只分析A这把锁的影响。更新都按办件编号查找,只改经办人、不改编号;没有其他索引、事务或数据库约束带来的冲突。
完成后核对每一步的原因
第一项需要等待。25号需要插入20与30之间,这个位置正被A的临键锁保护。
第二项也需要等待。临键锁的右端点30包含在内,A持有30号记录的X锁,所以B不能立即修改这条记录。
第三项不需要等待A这把锁。20号是这个区间排除的左端点,A给定的锁没有锁住20号记录;本题又没有其他冲突,所以B可以修改20号的经办人。
B.A失败后,需要重新执行哪些步骤
另一个业务要求把40、50号两件办件一起交接:两条记录要么都修改成功,要么都撤销。两条记录都已经存在,办件编号是主键。
A、B使用不同连接,各自开始可重复读事务,然后发生了下面的过程:
- A修改40号成功,但没有提交,仍持有40号记录的锁。
- B修改50号成功,但没有提交,仍持有50号记录的锁。
- A接着想修改50号,被B的锁挡住;随后B想修改40号,又被A的锁挡住。
- 数据库发现死锁,回滚了A整个事务。B随后完成两条修改并提交。
A的程序允许重新尝试这次交接,操作只包含数据库修改。请回答两点:
- A只重新执行“修改50号”够不够?如果不够,应该怎样重新开始?
- 如果A、B从一开始就都按“先修改40号、再修改50号”的顺序操作,为什么可以避免这里的互相等待?
完成后核对
只改50号不够。数据库回滚A整个事务时,也撤销了A之前对40号的修改。A需要开始一个新事务,重新检查两件办件当前是否允许交接,然后修改40和50,最后一起提交。若新的状态不允许交接,就应停止本次尝试。
统一顺序后,假设A先取得40号的锁,B会在修改40号这一步等待。B还没有走到修改50号那一步,所以没有占着50号的锁;A便可以继续修改50号并提交。这样就不会出现本题中A、B各自占着对方所需锁的情况。
可选双连接演示:亲眼观察已有主键记录上的等待
仅在已有隔离测试库使用两个独立连接。起初都自动提交、无活动事务;同名表已存在就停止核对,不删除覆盖。按标注切换连接,不能整段粘进一个窗口。任一步意外报错就停止,保留错误并回滚仍活动的事务;不要让测试事务长期留着。B在等待时,必须切回A释放锁,再等B语句返回。
-- 准备:连接A,无活动事务
CREATE TABLE teach_locks_0013 (
id INT PRIMARY KEY,
owner_id INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO teach_locks_0013 VALUES (10, 7), (20, 7), (30, 7);
-- ① 连接A:取得已有20的记录锁
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM teach_locks_0013 WHERE id = 20 FOR UPDATE;
-- ② 连接B:这条更新预期等待,先不要执行后续语句
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
UPDATE teach_locks_0013 SET owner_id = 9 WHERE id = 20;
-- ③ 切回连接A:结束事务、释放锁
COMMIT;
-- ④ 等连接B的更新成功返回后,在B中撤销测试修改
ROLLBACK;
SELECT owner_id FROM teach_locks_0013 WHERE id = 20;
-- 预期7,因为B的更新已撤销
验证目标:B先等待A,A提交后B继续,B回滚后值仍为7。若B已发生超时,不再假装走正常路径,应记录错误并回滚。此示例没有在示例环境中运行过,以上是依据官方规则推导的预期,自己运行时以实际结果为准;它也不验证临键区间或性能。
八、即时检验:做一次,再解释原因
每题点击后给出反馈。先合上参考答案,不用反复重刷来代替独立判断。
- A的这次锁定查询需要锁住哪条记录?
表里有10、20、30号办件,id是主键,也只有主键索引。A开始可重复读事务,执行 SELECT * FROM case_task WHERE id = 20 FOR UPDATE;,已经查到20号记录。只考虑本次查询对记录和间隙的加锁。
- A. 锁住表内全部记录
- B. 仅锁二十这条记录
- C. 锁住表内全部空隙
查看答案与解释
答案:B
id是唯一的主键,条件id = 20已经指定准确编号,而且20号确实存在,因此需要的是20号记录的记录锁,不需要它前面的间隙锁。
- B执行哪项操作时,需要等待A释放这把间隙锁?
表里有10、20、30号办件,A只保护10与20之间的插入位置,即持有 (10,20) 的纯间隙锁。A没有锁住20号记录本身。
B在另一个连接中操作。下面选项分别独立判断:查询是普通查询,更新按主键查找且不修改主键;没有其他锁或约束导致等待。
- A. 更新二十号的状态
- B. 查询二十号的状态
- C. 插入十五号新办件
查看答案与解释
答案:C
15落在空隙中,插入被阻挡。纯间隙锁不锁20记录,普通查询也不是申请冲突的记录锁。
- 为什么临键锁的范围写成
(10,20],右边用了方括号?
主键索引里已有10、20、30号记录。A取得20号记录上的X临键锁,范围表示为 (10,20]。请选择方括号表达的含义。
- A. 包含二十这条记录
- B. 排除二十这条记录
- C. 只管二十之后空隙
查看答案与解释
答案:A
右端点20包含在内,临键锁包括20记录与它前面的间隙;左端点10排除。
- 下面的操作顺序会形成什么等待关系?
A、B各自有一个尚未结束的事务,使用不同连接。A已经修改10号并持有它的X锁;B已经修改20号并持有它的X锁。
随后,A的更新语句想修改20号,因此等待B释放锁。A还没超时时,B又想修改10号,也需要取得X锁。没有其他参与者。
- A. 仅有单向等待关系
- B. 两个事务互相等待
- C. 两个事务已经提交
查看答案与解释
答案:B
A的更新要等B释放20号记录的锁,B的更新又要等A释放10号记录的锁,形成死锁。开启死锁检测时,数据库会选择其中一个完整事务回滚。
- A的程序可以重新尝试这次业务,应该重新执行哪些操作?
40、50号必须在同一事务里一起修改。A修改40号成功后,在修改50号时发生死锁;数据库选择回滚A,已经撤销A整个事务。业务只包含数据库修改,不包含发送消息等外部动作。
- A. 只重做最后失败语句
- B. 直接提交之前的修改
- C. 新事务重做整组操作
查看答案与解释
答案:C
A此前对40号的修改也已被撤销。因此A需要开始新事务,重新检查当前状态,再完成40和50两条修改,不能只重新执行修改50号。
- 为了避免只完成一半业务,A的程序现在应该怎么做?
A在同一显式事务中先修改10号成功,还没提交;接着修改20号时,因为等锁太久而报错。两条修改必须一起成功。
数据库使用默认超时回滚配置:只撤销超时的那条语句,没有自动回滚A整个事务。框架也还没有替A处理这个错误。
- A. 明确回滚整个事务
- B. 直接提交前面修改
- C. 忽略错误继续返回
查看答案与解释
答案:A
A对10号的修改可能仍留在原事务中。如果现在提交,就只完成了一半交接。因此A应回滚整个事务,再决定是否重新尝试。
九、把过程讲出来:用四问完整回答检验理解
先以看懂讲解为主。准备好后再回答,可以写下来,也可以先口述再整理成文字;不必一次做完。
第一问:为什么B修改20号要等,修改30号不用等
表中有10、20、30号办件,id是主键。A开始可重复读事务,用 WHERE id = 20 FOR UPDATE 查询20号,并且还没有结束事务。
B使用另一个连接,准备按办件编号修改经办人。分别考虑修改20号、修改30号,没有其他冲突。请说明两种操作为何不同,以及A的记录锁加在哪里。
第二问:新增15号和修改20号为什么都可能等待
另一个场景中,只有10、20、30号记录。A在事务中持有 (10,20] 的X临键锁。B在不同的小场景里分别尝试新增15号、修改20号的经办人;没有其他锁或约束干扰。
请分别解释这两个操作为什么需要等待。如果A改为只持有 (10,20) 的纯间隙锁,哪一个操作就不必再等这把锁?为什么?
第三问:两个事务怎样互相等住
A、B使用不同连接,准备各自在一个事务内修改10、20两条已有办件。A先修改10号,B先修改20号,都没有提交。随后A要修改20号,等待B;A还没超时时,B又要修改10号,等待A。数据库已开启死锁检测。
请说清双方各在等谁释放哪条记录的锁,数据库怎样解除这个死锁。如果你的整个事务被回滚,业务允许重试时,你要重新执行哪些步骤?
第四问:用户迟迟不确认,为什么B的更新也卡着
A已经锁定读取20号办件,却没有结束事务,而是等页面用户点击确认。B要修改20号,正在等A释放锁。A没有在等B或其他事务。
请解释为什么这还不是死锁,以及怎样调整“展示页面→用户确认→正式修改”的流程。假设正式修改包含两条必须一起成功的SQL,这两条应该继续放在同一个事务里吗?为什么?
练习:请在自己的笔记中写出答案(也可附第七节应用推演),再回到第二、三、五、六节的参考回答核对。
可选实验不是继续学习的前提;能跟着解释读懂、能独立讲出来、隔几天仍能答对,是三件不同的事。
十、集中追问:回到已经讲过的机制
先自己答,再展开
普通查询和锁定查询都叫SELECT,它们的保护一样吗?
不一样。在本课的读已提交和可重复读下,普通一致性读按照视图选择版本,不因此取得S记录锁。锁定查询需要先取得相应的锁;如果锁被其他事务占用,就可能等待。业务代码应在取得锁后检查读到的状态。
“完整唯一键等值命中已有记录”为什么不能缩成“有索引就只锁一行”?
因为有索引不等于完整唯一等值,也不保证命中已有记录;范围扫描、非唯一条件和未命中可能涉及间隙。实际路径还影响扫描范围。
纯间隙锁与X记录锁都叫锁,为什么冲突规则不同?
纯间隙锁主要阻止插入,可以与其他纯间隙锁共存;X记录锁保护已有记录,与其他事务在同记录上的S/X冲突。先分清保护对象,再用相应规则。
死锁被解决,是否意味着剩下的事务自动提交了?
不是。数据库回滚被选中的事务并释放它的锁以后,另一个事务的等待语句才有机会继续。另一个事务的程序仍需检查执行结果,最后提交或回滚自己的事务。
统一主键操作顺序是否保证永远没有死锁?
不能。它消除本例反向加锁的环,系统还可能在其他索引、表或路径上有冲突,所以仍需错误处理和有限重试。
十一、隔日复测:换数字,沿原方法判断
建议隔一天用五分钟完成,约一周后再换场景重做;刚看过答案就能复述,不等于已经记牢。
1. 主键已有40、60、80;A在显式事务中持有(40,60]的X临键锁。无其他冲突,B分别插入50、按主键修改40的经办人,谁被这把锁阻挡?
插入50被间隙部分阻挡;40是排除的左端点,按主键只修改40的经办人不被这把锁阻挡。
2. A已经修改40号,仍持有40号记录的锁,接着想改60号;B已经修改60号,仍持有60号的锁,接着想改40号。两者互相等待,数据库回滚A整个事务。A之前成功的修改还在吗?业务允许重试时应从哪里开始?
A在这个事务里此前完成的修改也被撤销。A需要开始新事务,重新检查当前状态,再完成40、60两条修改,不能只重新执行最后失败的那一条。
3. B等A的记录锁,但A不等任何其他事务,只是事务开着等页面确认。属于死锁吗?应该调整什么?
只有单向等待,没有等待环,不能称为死锁。避免把等待用户操作放在持锁事务中,真正修改前再在短事务里检查状态,并保留必要的原子边界。
第12课复习:A可重复读事务已普通查到旧值,B改值并提交。B的提交是否会刷新A已有的读视图?
不会。普通读取复用原视图,自己的写入另行判断;锁定读不能直接套用这套旧视图规则。
来源与配套材料
本课速查。示意锁区间是题设;双连接演示给出的是推导预期,不是实测输出。
学完后,挑一处仍不确定的地方,例如“15还不存在,为什么会等”或“为什么死锁后第一步也没了”,先按题设画出保护范围与等待关系,再回到对应小节核对处理办法。
课后问答:理解是怎样变化的
问题与结论默认可见。展开后可看理解过程;以下根据真实交流整理,并非逐字稿。
表中只有10、20、30号办件,办件编号是主键。A持有 (10,20] 的X临键锁时,B分别新增15号、修改20号的经办人,各自为什么要等待?如果A只持有 (10,20) 的纯间隙锁呢?
最终结论: 持有X临键锁时:新增15号因间隙部分受保护而等待;修改20号需要同一记录的X锁,因记录部分而等待。只持有纯间隙锁时,这把锁没有锁住20号记录,修改20号的经办人不必等它,但新增15号仍要等待。
展开理解过程与易混点
最初理解(归纳): 曾判断两种操作都要等待,并说出改成纯间隙锁后修改20号不再受这把锁阻挡;口述中把范围简称为“15到20”。
纠正与解释: “15到20”这种省略容易混淆被保护的范围。15要插入的位置属于10与20之间的间隙,应写成“15落在 (10,20) 之间”。临键锁 (10,20] 同时包含这段间隙和20号记录本身。
最终结论: 持有X临键锁时:新增15号因间隙部分受保护而等待;修改20号需要同一记录的X锁,因记录部分而等待。只持有纯间隙锁时,这把锁没有锁住20号记录,修改20号的经办人不必等它,但新增15号仍要等待。
现有复盘未逐条保留中间追问,本页不补写对话。
A持有10号的锁、等B释放20号;B持有20号的锁、等A释放10号。数据库已开启死锁检测,要先等到锁等待超时才会处理吗?
最终结论: 不需要。开启死锁检测时,数据库检测到循环等待就会选择一个事务回滚,撤销它此前的修改并释放它的锁。假设回滚的是A,B的等待语句才有机会继续,B之后仍要自己提交或回滚。
展开理解过程与易混点
最初理解(归纳): 曾正确说出双方各自在等哪条记录的锁,但把“先等到锁等待超时”当成了数据库处理这个死锁的前提。
纠正与解释: lock wait timeout(锁等待超时)描述的是一条语句等得超过时限,没有循环等待也可能超时。关闭死锁检测时,引擎可能依靠等待超时来打断等待,但这不是本题给定的情形;默认超时的回滚范围也不能直接当成死锁的整事务回滚。
最终结论: 不需要。开启死锁检测时,数据库检测到循环等待就会选择一个事务回滚,撤销它此前的修改并释放它的锁。假设回滚的是A,B的等待语句才有机会继续,B之后仍要自己提交或回滚。
现有复盘未逐条保留中间追问,本页不补写对话。
死锁后数据库回滚了A整个事务,业务允许重试。只重跑最后失败的那条更新可以吗?
最终结论: 不可以。在只含数据库修改的前提下,应开始新事务,重新检查必要的业务条件,完成两条修改,再一起提交;重试次数要有限。
展开理解过程与易混点
最初理解(归纳): 曾说明:数据库回滚整个事务时,前面成功执行的修改也被撤销,所以不能只重试最后一句。
纠正与解释: “执行成功过”不等于“已经提交”。A失败后,B可能已经完成了交接,上一次的业务判断不能直接沿用。
最终结论: 不可以。在只含数据库修改的前提下,应开始新事务,重新检查必要的业务条件,完成两条修改,再一起提交;重试次数要有限。
现有复盘未逐条保留中间追问,本页不补写对话。
A锁定读取20号后不结束事务,等页面用户点确认;B要修改20号,正在等A。这是死锁吗?让双方按同一顺序申请锁能解决吗?
最终结论: 不是死锁:只有B在等A,没有等待环。统一加锁顺序解决不了这里的问题;要改变的是A持锁等待用户的流程。缩短持锁时间与统一操作顺序可以同时使用,但要分别说清各自解决什么。
展开理解过程与易混点
最初理解(归纳): 曾正确指出A只是在等用户确认,没有在等B的锁;处理办法上曾提出让双方先申请同一条记录的锁,随后自己注意到这样“他们就会一直等”。
纠正与解释: 判断死锁看有没有循环等待,与“现在是否已经超时”无关。统一访问顺序用于避免两笔事务按相反顺序占用10号、20号这类循环;本题原本没有循环,A拿到锁后仍在等用户,B的等待时间并不会因此缩短。两种问题要分开:A、B各占对方所需的记录锁,是访问顺序相反造成循环等待,对策是让相关代码按一致顺序访问记录;A拿着记录锁等用户确认,是事务把用户思考和操作的时间也包了进来,对策是先等用户确认,再开启需要持锁的事务。
最终结论: 不是死锁:只有B在等A,没有等待环。统一加锁顺序解决不了这里的问题;要改变的是A持锁等待用户的流程。缩短持锁时间与统一操作顺序可以同时使用,但要分别说清各自解决什么。
现有复盘未逐条保留中间追问,本页不补写对话。
“展示页面→用户确认→正式修改”的流程,事务应该怎样安排?正式修改包含两条必须一起成功的SQL时又怎样处理?
最终结论: 展示页面时普通查询,查询完成后及时结束这次读取事务;等待用户确认期间不保持持有记录锁的事务;收到确认请求后开启新事务,锁定读取需要修改的办件并重新检查当前是否仍允许操作;两条必须一起成功的SQL仍放在同一事务中,全部成功后提交,失败则回滚并及时释放锁。
展开理解过程与易混点
纠正与解释: 页面上以前显示的状态可能已被别人改变,不能直接作为最终依据。缩短事务是把等待用户等无须持锁的时间移出去,不是把本来必须一起成功的两条修改拆开提交。官方建议也包括:缩短事务、尽快提交相关修改、保持一致的操作顺序。
最终结论: 展示页面时普通查询,查询完成后及时结束这次读取事务;等待用户确认期间不保持持有记录锁的事务;收到确认请求后开启新事务,锁定读取需要修改的办件并重新检查当前是否仍允许操作;两条必须一起成功的SQL仍放在同一事务中,全部成功后提交,失败则回滚并及时释放锁。
现有复盘未逐条保留中间追问,本页不补写对话。