看一次回复算我输 發表於 2025-7-8 09:31:38

SQL Server脏读防御指南

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、第一步:环境搭建&mdash;&mdash;给数据库装上&quot;零食监控器&quot;</a></li><li><a href="#_label1">二、第二步:复现脏读&mdash;&mdash;让数据上演&quot;偷吃现场&quot;</a></li><li><a href="#_label2">三、第三步:解决方案1&mdash;&mdash;用Read Committed隔离级别&quot;锁住零食袋&quot;</a></li><li><a href="#_label3">四、第四步:解决方案2&mdash;&mdash;用锁机制&quot;贴上封条&quot;</a></li><li><a href="#_label4">五、第五步:解决方案3&mdash;&mdash;用乐观锁&quot;防闺蜜偷吃&quot;</a></li><li><a href="#_label5">六、第六步:终极防御&mdash;&mdash;用快照隔离级别&quot;开监控录像&quot;</a></li><li><a href="#_label6">七、第七步:实战演练&mdash;&mdash;用代码验证所有方案</a></li></ul></div><p class="maodian"><a name="_label0"></a></p><h2>一、第一步:环境搭建&mdash;&mdash;给数据库装上&quot;零食监控器&quot;</h2>
<p><strong>目标</strong>:创建测试表,像准备零食一样准备好数据。</p>
<p><strong>步骤</strong>:</p>
<ol><li><strong>创建测试表</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">-- 创建模拟数据表(假设是"零食库存表")
CREATE TABLE Snacks
(
    Id INT PRIMARY KEY,
    Name NVARCHAR(50),
    Stock INT
);

-- 插入初始数据(袋装薯片库存50)
INSERT INTO Snacks VALUES (1, '薯片', 50);
</pre></div>
<ol start="2"><li><strong>开启两个会话</strong>:
<ul><li>在SQL Server Management Studio(SSMS)中打开两个查询窗口,分别模拟事务A和事务B。</li></ul></li></ol>
<p><strong>注释解析</strong>:</p>
<ul><li><code>Stock</code>字段模拟库存数量,初始值为50。</li><li>两个会话分别代表两个&quot;偷吃零食&quot;的事务。</li></ul>
<p class="maodian"><a name="_label1"></a></p><h2>二、第二步:复现脏读&mdash;&mdash;让数据上演&quot;偷吃现场&quot;</h2>
<p><strong>目标</strong>:用两个事务模拟脏读,像偷吃薯片后被发现一样。</p>
<p><strong>场景</strong>:</p>
<ul><li><strong>事务A</strong>:假装&quot;偷吃&quot;薯片,但还没提交。</li><li><strong>事务B</strong>:假装&quot;检查库存&quot;,发现被偷吃的数据。</li></ul>
<p><strong>代码示例(事务A窗口)</strong>:</p>
<div class="jb51code"><pre class="brush:sql;">-- 事务A:偷吃20袋薯片(但不提交!)
BEGIN TRANSACTION;

UPDATE Snacks
SET Stock = Stock - 20
WHERE Id = 1;

-- 暂停在此,等待事务B执行
WAITFOR DELAY '00:00:10'; -- 等待10秒让事务B有时间执行

ROLLBACK; -- 最终放弃偷吃(模拟回滚)
</pre></div>
<p><strong>代码示例(事务B窗口)</strong>:</p>
<div class="jb51code"><pre class="brush:sql;">-- 事务B:查看库存(可能会读到脏数据)
SELECT * FROM Snacks WHERE Id = 1;

-- 预期结果:Stock = 30(但事务A未提交!)
</pre></div>
<p><strong>现象</strong>:</p>
<ul><li>事务B会读到<code>Stock=30</code>,但事务A最终回滚,实际库存仍是50。</li><li>这就是脏读!就像偷吃薯片后又假装没动,但被监控拍到!</li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>三、第三步:解决方案1&mdash;&mdash;用Read Committed隔离级别&quot;锁住零食袋&quot;</h2>
<p><strong>目标</strong>:设置事务隔离级别,像给零食袋上锁一样防止未提交数据被读取。</p>
<p><strong>步骤</strong>:</p>
<ol><li><strong>在事务B中设置隔离级别</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">-- 在事务B窗口中,修改查询为:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;

SELECT * FROM Snacks WHERE Id = 1;

-- 结果:Stock始终显示50(脏读被阻止!)
COMMIT;
</pre></div>
<p><strong>注释解析</strong>:</p>
<ul><li><code>READ COMMITTED</code>:确保只能读取已提交的数据,像给零食袋加了&quot;已开封需付款&quot;的标签。</li><li>事务B现在会等待事务A提交或回滚,不会读取中间状态。</li></ul>
<p class="maodian"><a name="_label3"></a></p><h2>四、第四步:解决方案2&mdash;&mdash;用锁机制&quot;贴上封条&quot;</h2>
<p><strong>目标</strong>:用显式锁强制阻止脏读,像给零食袋贴上&quot;勿动&quot;封条。</p>
<p><strong>步骤</strong>:</p>
<ol><li><strong>在事务A中使用排他锁</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">-- 事务A:偷吃时立即加锁
BEGIN TRANSACTION;

UPDATE Snacks
SET Stock = Stock - 20
WHERE Id = 1
WITH (ROWLOCK, XLOCK); -- 行级排他锁

-- 等待期间,事务B无法读取此行!
WAITFOR DELAY '00:00:10';

ROLLBACK;
</pre></div>
<ol start="2"><li><strong>事务B尝试读取</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">-- 事务B:现在会阻塞,直到事务A释放锁
SELECT * FROM Snacks WHERE Id = 1;
</pre></div>
<p><strong>注释解析</strong>:</p>
<ul><li><code>XLOCK</code>:强制对行加排他锁,其他事务无法读取或修改。</li><li>这就像给零食袋贴上&quot;正在偷吃,请勿打扰&quot;的封条!</li></ul>
<p class="maodian"><a name="_label4"></a></p><h2>五、第五步:解决方案3&mdash;&mdash;用乐观锁&quot;防闺蜜偷吃&quot;</h2>
<p><strong>目标</strong>:用版本控制机制,像零食包装上的防伪码一样检测数据变化。</p>
<p><strong>步骤</strong>:</p>
<ol><li><strong>修改表结构,添加版本字段</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">ALTER TABLE Snacks
ADD Version INT DEFAULT 0; -- 版本号初始为0
</pre></div>
<ol start="2"><li><strong>事务A尝试偷吃并更新版本号</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">BEGIN TRANSACTION;

-- 读取当前版本
DECLARE @CurrentVersion INT;
SELECT @CurrentVersion = Version FROM Snacks WHERE Id = 1;

UPDATE Snacks
SET Stock = Stock - 20, Version = Version + 1
WHERE Id = 1 AND Version = @CurrentVersion; -- 检查版本是否一致

-- 模拟回滚
ROLLBACK;
</pre></div>
<ol start="3"><li><strong>事务B检查版本号</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">SELECT * FROM Snacks WHERE Id = 1;

-- 结果:版本号未变化,Stock仍为50
</pre></div>
<p><strong>注释解析</strong>:</p>
<ul><li>乐观锁通过版本号比对,确保只有未被修改的数据能被更新。</li><li>这就像零食包装上的防伪码,一撕就暴露&quot;偷吃痕迹&quot;!</li></ul>
<p class="maodian"><a name="_label5"></a></p><h2>六、第六步:终极防御&mdash;&mdash;用快照隔离级别&quot;开监控录像&quot;</h2>
<p><strong>目标</strong>:用快照隔离级别记录数据历史,像监控录像回放一样防偷吃。</p>
<p><strong>步骤</strong>:</p>
<ol><li><strong>在数据库级别启用快照隔离</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">-- 在SSMS中右键数据库 → 属性 → 选项 → 启用"允许快照隔离"
ALTER DATABASE YourDatabase SET ALLOW_SNAPSHOT_ISOLATION ON;
</pre></div>
<ol start="2"><li><strong>事务B使用快照隔离</strong>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;

SELECT * FROM Snacks WHERE Id = 1;

-- 即使事务A未提交,结果仍为50!
COMMIT;
</pre></div>
<p><strong>注释解析</strong>:</p>
<ul><li>快照隔离通过记录历史版本,让事务B看到事务A修改前的数据。</li><li>这就像监控录像回放,永远显示&quot;偷吃前&quot;的库存!</li></ul>
<p class="maodian"><a name="_label6"></a></p><h2>七、第七步:实战演练&mdash;&mdash;用代码验证所有方案</h2>
<p><strong>场景</strong>:模拟多个解决方案的实际效果。</p>
<p><strong>代码示例(事务A)</strong>:</p>
<div class="jb51code"><pre class="brush:sql;">-- 方案1:脏读发生
BEGIN TRANSACTION;
UPDATE Snacks SET Stock = 30 WHERE Id = 1;
-- 不提交,等待事务B读取
</pre></div>
<p><strong>代码示例(事务B)</strong>:</p>
<div class="jb51code"><pre class="brush:sql;">-- 方案1:脏读发生
SELECT * FROM Snacks; -- 读到30

-- 方案2:使用Read Committed
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM Snacks; -- 仍读到50!

-- 方案3:使用乐观锁
SELECT * FROM Snacks WHERE Version = 0; -- 确保未被修改
</pre></div>
<p>通过本文,你已经掌握了:</p>
<ol><li><strong>脏读的复现方法</strong>:用两个事务模拟&quot;偷吃&quot;与&quot;被偷吃&quot;。</li><li><strong>四大解决方案</strong>:隔离级别、显式锁、乐观锁、快照隔离。</li><li><strong>代码实战</strong>:从环境搭建到防御验证,覆盖所有关键步骤。</li></ol>
頁: [1]
查看完整版本: SQL Server脏读防御指南