5.1 ACID 与隔离级别:并发秩序的规格书


5.1 ACID 与隔离级别:并发秩序的规格书

本节摘要:隔离级别回答一个问题:允许并发事务互相"看见"到什么程度。本节先用脏读、不可重复读、幻读、丢失更新四个异象建立坐标系,再沿 SQL Server 五种隔离级别从松到紧走一遍阶梯,重点讲基于版本存储的快照系列如何用空间换并发。目标不是背级别,而是拿到业务需求就能选出最小够用的那档。

四个读异象,先建立坐标系

谈隔离级别之前得先说清"隔离不完美会发生什么"。脏读:读到了别的事务还没提交、随后又回滚的数据——报表把一笔作废的退款算进流水,就是典型。不可重复读:同一事务里两次读同一行,中间被别人改了提交,两次结果不同——对账逻辑因此对不上。幻读:同一事务里两次按同一条件范围查询,第二次多出或少了行——"查一遍没记录、插入时却报已存在"的灵异现场多源于此。丢失更新:两个事务都读旧值各自计算再写回,后写覆盖前写——库存扣减双双读到 10、各自减 1、最终剩 9。四个异象就是四种业务风险,隔离级别是给风险的定价表。

从松到紧的阶梯

未提交读(READ UNCOMMITTED)完全不锁读,脏读都可能发生,只配给"错了也无妨"的场景(比如粗粒度的监控大盘)。已提交读(READ COMMITTED)是默认档:读之前拿共享锁、读完就放,保证不脏读;但同一事务内两次读之间锁已释放,不可重复读与幻读照旧。可重复读(REPEATABLE READ)把共享锁持到事务结束,挡住不可重复读,幻读仍在。可序列化(SERIALIZABLE)用范围锁把查询区间整个封住,三个异象全部杜绝,代价是并发度骤降、死锁概率上升,只给账务核心这类"必须串行语义"的窄场景。

快照系列是另一条路:已提交读快照(READ_COMMITTED_SNAPSHOT 数据库选项)让读语句不拿锁、转而读取该行上一次提交的版本,写不阻塞读、读不阻塞写;快照隔离(SNAPSHOT 事务级)更进一步,整个事务都站在事务开始的版本快照上,顺带消掉不可重复读,代价是更新冲突检测——两个事务基于同一快照改同一行,后提交者收到冲突报错必须重试。版本存储住在 tempdb,这也是第 3 章"tempdb 是公共背锅侠"的又一出处:开快照后 tempdb 的版本存储增长要纳入监控。

-- 打开快照读:一行配置,读负载重的库往往立竿见影 ALTER DATABASE 订单库 SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; -- 查看当前所有库的隔离体系配置 SELECT name, snapshot_isolation_state_desc AS 快照隔离, is_read_committed_snapshot_on AS 已提交读快照 FROM sys.databases;
-- 复现丢失更新与解决方案:带版本判断的原子更新 -- 危险写法(应用层读-改-写): -- SELECT Stock FROM t WHERE Sku=@sku; 应用算出 10-1=9 UPDATE t SET Stock=9 ... -- 正确写法:把计算下推为原子操作 UPDATE t SET Stock = Stock - @qty WHERE Sku = @sku AND Stock >= @qty; IF @@ROWCOUNT = 0 RAISERROR('库存不足或并发冲突', 16, 1); -- 或乐观锁版本号:UPDATE ... SET Stock=9, Ver=Ver+1 WHERE Sku=@sku AND Ver=@旧版本;

选择方法论:从异象倒推级别

办案口径是把业务动作翻译成异象清单再倒推级别。柜台扣款:丢失更新零容忍,原子 UPDATE 或快照隔离加重试。跨页报表:脏读会造成汇总口径漂移,至少已提交读;读压力大的报表库直接开已提交读快照,用 tempdb 的空间换报表与交易的互不排队。对账复核:两次读必须一致,可重复读或快照隔离。票据生成这类"存在性判定":可序列化或改用唯一约束兜底。一句话原则:默认档不动,按动作升级,快照优先于范围锁——先想快照系列能不能解决,再考虑可序列化,最后才考虑应用级队列。

⚠️ 快照系列不是免费的午餐:版本链清理依赖"没有超长事务",一个忘提交的连接能让版本存储持续膨胀,拖垮 tempdb。开快照的库,必须同步上"长事务告警"。

快照体系的运行成本对账

开快照前算清三笔账,避免上线后再返工。tempdb 账:版本存储的增长与"写入量乘以读事务时长"正相关——读事务越长、写入越频繁,版本堆积越快;启用后观察版本存储的占用曲线与清理速率,两者差值持续为正就是长事务在作祟。冲突账:快照隔离的更新冲突率取决于写写重叠概率,上线前用业务高峰的写模式估一估,冲突率超过个位数百分比就要考虑应用重试的代价。语义账:读不再加锁意味着读到的可能是稍旧版本,报表口径从"此刻一致"变成"快照一致",多数报表场景无感,但跨系统的对账逻辑要知道这个差别。
应用侧的重试范式也在此一并交代:捕获冲突错误后,指数退避加重试是标准姿势(先等一百毫秒,每次翻倍,封顶五次),重试耗尽再报错人工介入。把重试逻辑封装在数据访问层统一处理,比散落在各业务代码里可靠得多——这是快照体系能平稳运行的最后一块拼图。

隔离级别的变更管理

隔离级别有一个容易忽略的属性:它是数据库级与语句级的双重体系,变更必须分级管理。数据库级的快照开关影响所有会话,属于重大变更——要在维护窗口执行(开启时最好配合滚动终止策略)、要先评估 tempdb 容量、要预留回退语句;语句级的级别覆盖只影响单条查询,属于局部变更,但散落在代码里的表提示同样要进代码评审,防止某个查询悄悄升到可序列化拖累全表。两类变更都写进基线(第 8 章的配置基线),漂移能被月度巡检抓到——隔离级别这种"看不见的性能旋钮",最怕的就是没人记得它被谁动过。

本节要点回顾

  • 四个异象是坐标:脏读、不可重复读、幻读、丢失更新,先列业务风险清单再谈级别;
  • 默认档已提交读:读完即放锁,防脏读但不管重复读与幻读;
  • 快照系列换并发:读走版本存储不拿锁,写写冲突靠检测加重试,tempdb 成本入账;
  • 可序列化是窄门:范围锁并发代价高,只给必须串行语义的核心场景;
  • 原子化更新是第一防线:把"读-改-写"下推为单条 UPDATE,多数丢失更新无需升级隔离级别;
  • 长事务是新风险:快照体系对超长事务敏感,配置与告警必须成对上线。

规格书读完了,下一节看秩序的实际执行:锁怎么申请、阻塞链怎么还原、死锁现场怎么尸检——这是本章最高频的办案技能。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U