MySQL 锁机制全解析从锁的分类到并发更新是否阻塞(最新推荐)
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">引言</a></li><li><a href="#_label1">一、MySQL 有哪些锁?——三大类锁体系</a></li><li><a href="#_label2">二、表锁和行锁解决了什么问题?</a></li><li><a href="#_label3">三、并发场景实战分析:到底会不会阻塞?</a></li><li><a href="#_label4">总结</a></li></ul></div><p class="maodian"><a name="_label0"></a></p><h2>引言</h2><p>在并发场景下,数据库需要解决三类核心问题:</p>
<ul><li><strong>数据一致性</strong>:不能读到脏数据</li><li><strong>并发安全</strong>:多个事务同时修改数据时不出错</li><li><strong>性能权衡</strong>:在一致性和并发性能之间取得平衡</li></ul>
<p>MySQL(准确来说是 <strong>InnoDB 存储引擎</strong>)通过一整套<strong>多层次锁机制</strong>来解决这些问题。这些锁并不是“随便加的”,而是紧密围绕 <strong>事务隔离级别、索引结构、并发访问模式</strong> 设计出来的。</p>
<p>本文将从三个层次展开:</p>
<ul><li>MySQL 有哪些锁?</li><li>表锁和行锁到底解决了什么问题?</li><li>真实并发场景下,SQL 会不会阻塞?为什么?</li></ul>
<p class="maodian"><a name="_label1"></a></p><h2>一、MySQL 有哪些锁?——三大类锁体系</h2>
<p>从<strong>作用范围</strong>来看,MySQL 的锁可以分为三大类:</p>
<blockquote><p><strong>全局锁 → 表级锁 → 行级锁</strong></p></blockquote>
<h4>全局锁(Global Lock)</h4>
<h5>1.1 什么是全局锁?</h5>
<p>全局锁会<strong>锁住整个数据库实例</strong>,使其进入只读状态。</p>
<p>最典型的加锁方式是:</p>
<div class="jb51code"><pre class="brush:sql;">FLUSH TABLES WITH READ LOCK;</pre></div>
<p>此时:</p>
<ul><li>所有表都只能读</li><li>所有写操作(INSERT / UPDATE / DELETE)都会被阻塞</li></ul>
<h5>1.2 全局锁的典型使用场景</h5>
<p><strong>逻辑备份</strong></p>
<div class="jb51code"><pre class="brush:sql;">mysqldump --single-transaction ...</pre></div>
<p>在早期 MySQL 版本中,为了保证备份过程中数据一致,通常需要加全局读锁。</p>
<h5>1.3 全局锁的问题</h5>
<ul><li>整库不可写</li><li>在线业务几乎不可接受</li></ul>
<p><strong>现在更多使用 MVCC + 一致性快照,避免全局锁</strong></p>
<h4>表级锁(Table-level Lock)</h4>
<p>表级锁的作用范围是 <strong>单张表</strong>,开销小,但并发能力有限。</p>
<p>2.1 表锁(Table Lock)</p>
<div class="jb51code"><pre class="brush:sql;">LOCK TABLE t_order READ;
LOCK TABLE t_order WRITE;
</pre></div>
<ul><li>READ:其他线程可读,不可写</li><li>WRITE:其他线程既不可读,也不可写</li></ul>
<p>InnoDB <strong>一般不推荐使用显式表锁</strong></p>
<h5>2.2 元数据锁(MDL,Metadata Lock)</h5>
<p>MDL 是<strong>自动加的锁</strong>,很多人“被它坑过,但不知道是它”。</p>
<ul><li>对表进行 <strong>CRUD</strong> → 加 <strong>MDL 读锁</strong></li><li>对表进行 <strong>DDL(ALTER / DROP)</strong> → 加 <strong>MDL 写锁</strong></li></ul>
<p><strong>读写互斥!</strong></p>
<div class="jb51code"><pre class="brush:sql;">-- 事务 A
SELECT * FROM t_order; -- 持有 MDL 读锁
-- 事务 B
ALTER TABLE t_order ADD COLUMN xxx; -- 等待</pre></div>
<p><strong>线上 DDL 阻塞业务的元凶</strong></p>
<h5>2.3 意向锁(Intention Lock)</h5>
<p>意向锁是 <strong>InnoDB 行锁的“前置声明”</strong>,存在于表级别。</p>
<ul><li>意向共享锁(IS)</li><li>意向排他锁(IX)</li></ul>
<p><strong>作用:让表锁与行锁能快速判断是否冲突</strong></p>
<blockquote><p>“我要锁某几行了,你别直接给我整个表上锁”</p></blockquote>
<h4>行级锁(Row-level Lock)</h4>
<p>行锁是 <strong>InnoDB 的核心竞争力</strong>,并发性能的关键。</p>
<h5>3.1 记录锁(Record Lock)</h5>
<p>锁住 <strong>某一条索引记录</strong></p>
<ul><li><strong>S 锁(共享锁)</strong>:读</li><li><strong>X 锁(排他锁)</strong>:写</li></ul>
<div class="jb51code"><pre class="brush:sql;">SELECT * FROM t_order WHERE id = 10 LOCK IN SHARE MODE;
UPDATE t_order SET status = 1 WHERE id = 10;</pre></div>
<h5>3.2 间隙锁(Gap Lock)</h5>
<p>锁住的是 <strong>索引区间之间的“空隙”</strong></p>
<ul><li><strong>只存在于 RR(可重复读)隔离级别</strong></li><li>目的:<strong>防止幻读</strong></li></ul>
<div class="jb51code"><pre class="brush:sql;">SELECT * FROM t_order WHERE id BETWEEN 10 AND 20 FOR UPDATE;
</pre></div>
<p>锁住 <code>(10, 20)</code> 这个区间,阻止插入新记录</p>
<p><strong>间隙锁之间是兼容的</strong></p>
<h5>3.3 临键锁(Next-Key Lock)</h5>
<p><strong>Next-Key Lock = Record Lock + Gap Lock</strong></p>
<ul><li>锁住记录本身</li><li>同时锁住前面的间隙</li></ul>
<p>这是 <strong>InnoDB RR 隔离级别的默认加锁策略</strong></p>
<p class="maodian"><a name="_label2"></a></p><h2>二、表锁和行锁解决了什么问题?</h2>
<table><thead><tr><th>锁类型</th><th>解决的问题</th></tr></thead><tbody><tr><td>表锁</td><td>DDL 与 DML 冲突</td></tr><tr><td>行锁</td><td>并发更新同一行</td></tr><tr><td>间隙锁</td><td>防止幻读</td></tr><tr><td>临键锁</td><td>范围查询 + 插入一致性</td></tr></tbody></table>
<p>本质目标只有一个:<br /><strong>在并发环境下,保证事务隔离语义成立</strong></p>
<p class="maodian"><a name="_label3"></a></p><h2>三、并发场景实战分析:到底会不会阻塞?</h2>
<p>场景 1:两个事务同时 UPDATE 同一条记录</p>
<div class="jb51code"><pre class="brush:sql;">UPDATE t_order SET status = 1 WHERE id = 100;
</pre></div>
<p><strong>会阻塞</strong></p>
<ul><li>第一个事务获取 id=100 的 X 锁</li><li>第二个事务只能等待</li></ul>
<p>场景 2:更新不同主键的记录</p>
<div class="jb51code"><pre class="brush:sql;">-- 事务 A
UPDATE t_order SET status = 1 WHERE id = 100;
-- 事务 B
UPDATE t_order SET status = 1 WHERE id = 200;</pre></div>
<p><strong>不会阻塞</strong></p>
<ul><li>行锁粒度是“记录级”</li><li>主键索引精确定位</li></ul>
<p>场景 3:更新主键范围(存在索引)</p>
<div class="jb51code"><pre class="brush:sql;">UPDATE t_order SET status = 1 WHERE id BETWEEN 100 AND 200;
</pre></div>
<ul><li>加的是 <strong>Next-Key Lock</strong></li><li>会锁住区间</li></ul>
<p><strong>其他事务在该区间 INSERT 会被阻塞</strong></p>
<p>场景 4:WHERE 条件未命中索引</p>
<div class="jb51code"><pre class="brush:sql;">UPDATE t_order SET status = 1 WHERE order_no = 'xxx';
</pre></div>
<p><strong>如果 order_no 没有索引</strong></p>
<ul><li>InnoDB 会 <strong>全表扫描</strong></li><li>锁升级为 <strong>锁住大量记录甚至整表</strong></li></ul>
<p><strong>这才是线上“莫名其妙阻塞”的根源</strong></p>
<p class="maodian"><a name="_label4"></a></p><h2>总结</h2>
<h4>锁不是 SQL 层决定的,而是:</h4>
<blockquote><p><strong>SQL + 索引 + 隔离级别 + 引擎共同作用的结果</strong></p></blockquote>
<h4>关键记忆点</h4>
<ul><li>RR 隔离级别 ≠ 只有行锁</li><li><strong>Next-Key Lock 是默认策略</strong></li><li>无索引 ≈ 放弃行锁优势</li><li>MDL 是 DDL 阻塞的幕后黑手</li></ul>
<h4>实战建议</h4>
<ul><li>所有更新 SQL <strong>必须走索引</strong></li><li>范围更新要评估间隙锁影响</li><li>线上 DDL 使用 Online DDL</li><li>用 <code>show engine innodb status</code> 排查锁等待</li></ul>
頁:
[1]