华健 發表於 2025-7-23 17:36:00

MySQL的并发访问机制

<p>在MySQL中,锁是用于解决并发访问冲突的核心机制。当多个事务同时操作数据库中的数据时(如读取、修改、删除),可能会出现数据不一致(如脏读、不可重复读、幻读)或操作冲突(如同时修改同一行),锁的作用就是通过合理限制不同事务的操作权限,保证数据的一致性和并发操作的正确性。本文只讨论InnoDB引擎下并发访问控制。</p>
<h3 id="锁的粒度">锁的粒度</h3>
<p>MySQL从锁的粒度上分为表级锁和行级锁,MySQL会根据不同情况判断是对表上锁还是对行上锁。</p>
<h4 id="表级锁">表级锁</h4>
<p>表级锁有四种类型:</p>
<ul>
<li>共享锁
<ul>
<li>显示上锁 : LOCK TABLES 表名 READ;UNLOCK TABLES;</li>
</ul>
</li>
<li>排他锁
<ul>
<li>显示上锁 : LOCK TABLES 表名 WRITE;UNLOCK TABLES;</li>
<li>隐式上锁:如果行级锁需要加锁的数据条数太多,会直接加表锁,减少维护行锁的消耗。</li>
</ul>
</li>
<li>意向共享锁
<ul>
<li>给行加共享锁会尝试给表加意向共享锁</li>
</ul>
</li>
<li>意向排他锁
<ul>
<li>给行加排他锁会尝试给表加意向排他锁</li>
</ul>
</li>
</ul>
<blockquote>
<p>如果没有意向锁,当一个事务想要给整个表加表级排他锁(X 锁)时,需要逐行检查是否有行级锁(S 锁或 X 锁),这在大表中会非常低效。而意向锁通过 “预先声明” 的方式,让表级锁的检查只需判断意向锁的类型,无需扫描每行,大幅提升性能。</p>
</blockquote>
<h4 id="行级锁">行级锁</h4>
<p>行级锁粒度:</p>
<ul>
<li>行锁 对每一行数据加锁</li>
<li>间隙锁区域锁,防止其他事务数据插入导致的幻读。</li>
<li>next-key 同时给行锁和其前面的间隙加锁。</li>
</ul>
<blockquote>
<p>间隙锁和Next-Key锁(行级+间隙)只存在不小于RR(可重复读)隔离级别下,用于解决幻读,下面有论述。</p>
</blockquote>
<p>行级锁类型:</p>
<ul>
<li>共享锁
<ul>
<li>显示加锁:SELECT ... LOCK IN SHARE MODE</li>
<li>隐式加锁:串行化隔离级别时候,读取数据会加共享锁,确保整个事务内读取的数据不会变化</li>
</ul>
</li>
<li>排他锁
<ul>
<li>显示加锁:SELECT ... FOR UPDATE</li>
<li>隐式加锁:尝试更新数据的时候</li>
</ul>
</li>
</ul>
<blockquote>
<p>共享锁之间不冲突,多个事务可以对同一个表或数据加共享锁。共享锁和排他锁之间冲突,A事务对一个表或数据加了共享锁,B事务就无法再在这个表或数据上加排他锁。显示的加共享锁,可以防止某些数据被其他事务更新,可以在可重复读隔离级别下实现串行化。</p>
</blockquote>
<h3 id="不同隔离级别的上锁逻辑">不同隔离级别的上锁逻辑</h3>
<p>不同隔离级别的时候的上锁逻辑不一样的。</p>
<h4 id="读取数据">读取数据</h4>
<table>
<thead>
<tr>
<th style="text-align: left"><strong>隔离级别</strong></th>
<th style="text-align: left"><strong>是否默认加S锁</strong></th>
<th style="text-align: left"><strong>显式加锁方式(强制加锁)</strong></th>
<th style="text-align: left"><strong>主要目的</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">READ UNCOMMITTED</td>
<td style="text-align: left">否</td>
<td style="text-align: left">无(通常无需)</td>
<td style="text-align: left">允许脏读,追求极致并发</td>
</tr>
<tr>
<td style="text-align: left">READ COMMITTED</td>
<td style="text-align: left">否</td>
<td style="text-align: left"><code>SELECT ... FOR SHARE</code></td>
<td style="text-align: left">避免脏读,不阻塞读写</td>
</tr>
<tr>
<td style="text-align: left">REPEATABLE READ</td>
<td style="text-align: left">否(依赖 MVCC)</td>
<td style="text-align: left"><code>SELECT ... FOR SHARE</code></td>
<td style="text-align: left">默认无锁,显式加锁用于特殊场景</td>
</tr>
<tr>
<td style="text-align: left">SERIALIZABLE</td>
<td style="text-align: left">是(隐式加 S 锁)</td>
<td style="text-align: left">无需显式,自动加锁</td>
<td style="text-align: left">完全串行化,保证最高一致性</td>
</tr>
</tbody>
</table>
<h4 id="修改数据">修改数据</h4>
<table>
<thead>
<tr>
<th style="text-align: left"><strong>隔离级别</strong></th>
<th style="text-align: left"><strong>是否加行锁(X 锁)</strong></th>
<th style="text-align: left"><strong>是否加间隙锁 / Next-Key Lock</strong></th>
<th style="text-align: left"><strong>主要目的</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">READ UNCOMMITTED</td>
<td style="text-align: left">是</td>
<td style="text-align: left">否</td>
<td style="text-align: left">防止并发修改冲突</td>
</tr>
<tr>
<td style="text-align: left">READ COMMITTED</td>
<td style="text-align: left">是</td>
<td style="text-align: left">否</td>
<td style="text-align: left">防止并发修改冲突</td>
</tr>
<tr>
<td style="text-align: left">REPEATABLE READ</td>
<td style="text-align: left">是</td>
<td style="text-align: left">可能(范围 / 未命中时)</td>
<td style="text-align: left">防止并发修改 + 幻读</td>
</tr>
<tr>
<td style="text-align: left">SERIALIZABLE</td>
<td style="text-align: left">是</td>
<td style="text-align: left">是(范围 / 未命中时)</td>
<td style="text-align: left">最高一致性,彻底防幻读</td>
</tr>
</tbody>
</table>
<h3 id="mvcc">MVCC</h3>
<p>MVCC 是 InnoDB 存储引擎实现非阻塞读的关键机制,其核心思想是:为数据维护多个版本,事务读取时无需加锁,而是通过 “版本链” 找到符合自身可见性规则的数据版本,从而避免读操作阻塞写操作(反之亦然),提升并发性能。</p>
<h4 id="mvcc-在不同隔离级别的表现">MVCC 在不同隔离级别的表现</h4>
<ol>
<li>读已提交(RC)隔离级别</li>
</ol>
<p>MVCC 生效:读操作(SELECT)会通过 MVCC 读取 “已提交的最新版本” 数据。<br>
具体行为:</p>
<ul>
<li>每次执行 SELECT 时,都会生成一个新的 Read View(读视图,用于判断数据版本的可见性)。</li>
<li>因此,同一事务中两次执行相同的 SELECT,可能读到其他事务已提交的新数据(即 “不可重复读”)。</li>
<li>例如:事务 A 第一次查询某行值为 1,事务 B 修改为 2 并提交,事务 A 再次查询会读到 2。</li>
<li>MVCC 的作用:避免读操作加锁,同时保证不会读到未提交的脏数据(符合 “读已提交” 的要求)。</li>
</ul>
<ol start="2">
<li>可重复读(RR)隔离级别</li>
</ol>
<p>MVCC 生效:读操作通过 MVCC 读取 “事务启动时的一致性版本” 数据。</p>
<p>具体行为:</p>
<ul>
<li>事务启动时生成一个 Read View,并在整个事务期间复用该视图,不再重新生成。</li>
<li>因此,同一事务中多次执行 SELECT 会读到相同的数据(即使其他事务已提交修改),实现 “可重复读”。</li>
<li>例如:事务 A 启动时查询某行值为 1,事务 B 修改为 2 并提交,事务 A 再次查询仍读到 1。</li>
<li>MVCC 的作用:在无锁的情况下,保证事务内读取数据的一致性,同时避免 “不可重复读”。</li>
</ul>
<h3 id="不同隔离级别的实现原理">不同隔离级别的实现原理</h3>
<table>
<thead>
<tr>
<th>事务级别</th>
<th>描述</th>
<th>实现</th>
</tr>
</thead>
<tbody>
<tr>
<td>READ UNCOMMITTED</td>
<td>能够读取到其他事务未提交的数据</td>
<td>直接读取数据的最新版本。</td>
</tr>
<tr>
<td>READ COMMITTED</td>
<td>不能读取到未提交数据,同一事务里多次读取可能查询的数据不一样</td>
<td>通过MVCC快照实现,排除掉正在执行的事务,读取当前事务之前提交的版本。但是重新读取的时候会重新生成readview,所以会读取到已提交的数据。</td>
</tr>
<tr>
<td>REPEATABLE READ</td>
<td>同一事务下每次读取的数据保持一致</td>
<td>读取的时候通过mvvc创建一个readview,再次读取的时候使用之前readview,所以不会查询到事务执行期间提交的数据。特殊情况下使用间隙锁保证,下面有描述。</td>
</tr>
<tr>
<td>SERIALIZABLE</td>
<td>同一事务下每次读取的数据保持一致</td>
<td>通过select加读锁保证数据不被修改。</td>
</tr>
</tbody>
</table>
<p>事务隔离级别是用于控制不同事务级别下同一事务读取数据的逻辑。更新数据的时候因为需要更新最新版本的数据,无法使用MVCC,还是需要靠锁进行数据隔离。</p>
<h3 id="mvcc和间隙锁">MVCC和间隙锁</h3>
<p>MVCC已经能解决幻读的问题了,为什么还要有间隙锁。具体场景分析:</p>
<p>假设表<code>user</code>的<code>id</code>(主键)存在记录<code>10</code>、<code>20</code>,初始数据如下:</p>
<table>
<thead>
<tr>
<th style="text-align: left"><strong>id</strong></th>
<th style="text-align: left"><strong>name</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">10</td>
<td style="text-align: left">Alice</td>
</tr>
<tr>
<td style="text-align: left">20</td>
<td style="text-align: left">Bob</td>
</tr>
</tbody>
</table>
<p><font style="color: rgba(0, 0, 0, 1)">步骤 1:事务 A 启动,执行快照读(普通 SELECT)</font></p>
<p>事务 A 开始后,执行第一次范围查询(快照读,依赖 MVCC):</p>
<p><strong>sql</strong></p>
<pre><code class="language-sql">-- 事务A
START TRANSACTION;
-- 快照读:基于MVCC的Read View,只能看到事务启动前已提交的数据
SELECT * FROM user WHERE id BETWEEN 10 AND 20;
-- 结果:仅能看到id=10和id=20的记录
</code></pre>
<p><font style="color: rgba(0, 0, 0, 1)">步骤 2:事务 B 插入新记录并提交</font></p>
<p>此时事务 B 插入一条在<code></code>范围内的新记录,并提交:</p>
<p><strong>sql</strong></p>
<pre><code class="language-sql">-- 事务B
START TRANSACTION;
INSERT INTO user (id, name) VALUES (15, 'Charlie'); -- 插入新记录
COMMIT;
</code></pre>
<p>由于没有间隙锁,事务 B 的插入操作不会被阻塞(间隙锁的作用就是阻止这种插入)。</p>
<p><font style="color: rgba(0, 0, 0, 1)">步骤 3:事务 A 执行当前读(如 UPDATE / 加锁查询)</font></p>
<p>事务 A 接着执行一个 “当前读” 操作(如更新或加锁查询,会读取最新数据,而非 MVCC 快照):</p>
<p><strong>sql</strong></p>
<pre><code class="language-sql">-- 事务A
-- 当前读:读取最新数据,而非快照
UPDATE user SET name = 'Updated' WHERE id BETWEEN 10 AND 20;
-- 此时会更新id=10、20(原有记录)和id=15(事务B插入的新记录)
</code></pre>
<p>步骤 4:事务 A 再次执行快照读,出现幻读</p>
<p>事务 A 再次执行快照读时:</p>
<p><strong>sql</strong></p>
<pre><code class="language-sql">-- 事务A
SELECT * FROM user WHERE id BETWEEN 10 AND 20;
</code></pre>
<p>此时结果会包含<code>id=15</code>的记录(因为事务 A 自己更新过这条记录,MVCC 规则中 “事务可以看到自己修改的内容”)。</p>
<p>但这条记录在事务 A 第一次查询时并不存在,因此出现了 “幻读”—— 同一事务内,两次相同的范围查询,第二次出现了第一次未见过的新记录。</p><br><br>
来源:https://www.cnblogs.com/soker/p/19001165
頁: [1]
查看完整版本: MySQL的并发访问机制