Skip to content

READ UNCOMMITTED / READ COMMITTED ​

READ COMMITTED 是 PostgreSQL 的默认隔离级别,也是绝大多数应用的合理选择。它的核心是「语句级快照」:每条语句在开始执行的那一刻取一个快照,只看到该时刻之前已提交的数据。PostgreSQL 中请求 READ UNCOMMITTED 会被静默提升为 READ COMMITTED——因为 PG 的 MVCC 实现从根上不允许脏读,标准里最弱的那个级别在这里根本不存在。

前置知识

阅读本节前,建议先了解:隐式与显式事务、MVCC 概述

基础概念 ​

先纠正一个最常见的预期偏差:

你以为PostgreSQL 18 的真实行为
READ UNCOMMITTED 能读到未提交数据不可能:PG 把它当作 READ COMMITTED
READ COMMITTED 的行为与 READ UNCOMMITTED 相同语义上等同,但没有任何脏读
同一事务内两次读同一行结果一定相同不一定:语句级快照,中间别人提交就会变
SELECT 读到的行 UPDATE 时一定还能改不一定:若已被别人改动,会阻塞并重新检查

四个隔离级别在 PostgreSQL 中的实际映射:

请求的级别PostgreSQL 实际使用脏读不可重复读幻读序列化异常
READ UNCOMMITTEDREAD COMMITTED不可能可能可能可能
READ COMMITTED(默认)READ COMMITTED不可能可能可能可能
REPEATABLE READREPEATABLE READ(快照隔离)不可能不可能不可能可能
SERIALIZABLESERIALIZABLE(SSI)不可能不可能不可能不可能

READ COMMITTED 的两条核心规则:

text
规则 1(读):每条语句开始时取新快照
  → 同一事务内两次 SELECT 之间若别人提交了修改,第二次能看到新值

规则 2(写):UPDATE / DELETE 时若目标行已被并发事务修改
  → 阻塞等待对方结束
  → 对方提交:PG 用最新版本「重新检查」WHERE 条件(EvalPlanQual)
  → 对方回滚:正常更新

语法与用法 ​

text
-- 事务级设置(必须在事务开始后、任何查询之前)
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
COMMIT;

-- 也可在 BEGIN 时直接指定
BEGIN ISOLATION LEVEL READ UNCOMMITTED;   -- PG 视为 READ COMMITTED
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 会话级设置(持续到会话结束或再次修改)
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 查看当前级别
SHOW transaction_isolation;

最简可运行示例:

sql
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
COMMIT;
text
BEGIN
 balance
---------
 1000.00
(1 row)

COMMIT
sql
SHOW default_transaction_isolation;
text
 default_transaction_isolation
-------------------------------
 read committed
(1 row)

详细说明 ​

语句级快照:READ COMMITTED 的读取语义 ​

READ COMMITTED 的快照不是「事务开始时」取的,而是「每条语句开始时」取的。这意味着同一事务里的两条 SELECT 可能看到不同版本的数据:

text
会话 A(READ COMMITTED):
BEGIN;
SELECT balance FROM accounts WHERE id = 1;   -- 读到 1000.00
-- 此刻会话 B 提交了修改(balance 改为 800.00)
SELECT balance FROM accounts WHERE id = 1;   -- 读到 800.00(不可重复读)
COMMIT;
text
-- 会话 A 输出
BEGIN
 balance
---------
 1000.00
(1 row)

 balance
---------
  800.00
(1 row)

COMMIT
text
-- 会话 B 在此期间执行
BEGIN;
UPDATE accounts SET balance = 800.00 WHERE id = 1;
COMMIT;

这就是不可重复读(non-repeatable read):同一事务内读同一行得到不同结果。它不违反 READ COMMITTED 的契约,因为这个级别的契约只管「不读未提交数据」,不管「读的一致性」。

不可重复读:为什么它经常是 bug 源头 ​

单看「读两次不一样」似乎无害,但当它与应用逻辑结合时就会出事:

python
# 危险:READ COMMITTED 下的「检查—使用」间隙
cur.execute("SELECT status FROM orders WHERE id = 42")   # 读到 'pending'
# 此刻另一个事务把它改成 'cancelled' 并提交
if status == "pending":                                  # 基于过期信息判断
    cur.execute("UPDATE orders SET status = 'shipped' WHERE id = 42")
    # 把一个已取消的订单发货了
sql
-- SQL 层面的等价复现
BEGIN;   -- READ COMMITTED(默认)
SELECT status FROM orders WHERE id = 42;         -- 'pending'
SELECT status FROM orders WHERE id = 42;         -- 可能已是 'cancelled'
UPDATE orders SET status = 'shipped' WHERE id = 42;
COMMIT;
text
BEGIN
 status
---------
 pending
(1 row)

  status
-----------
 cancelled
(1 row)

UPDATE 1
COMMIT

修正方式只有两类:把判断与写入合并为一条原子语句,或用锁把中间间隙封住:

sql
-- 方式 1:条件写(把判断下推到 WHERE,由 PG 保证原子性)
UPDATE orders SET status = 'shipped'
WHERE id = 42 AND status = 'pending';
-- 受影响行数为 0 表示条件不满足,业务据此判断
text
UPDATE 0
sql
-- 方式 2:显式加锁,把间隙封住
BEGIN;
SELECT status FROM orders WHERE id = 42 FOR UPDATE;   -- 加行锁
UPDATE orders SET status = 'shipped' WHERE id = 42;
COMMIT;
text
BEGIN
 status
---------
 pending
(1 row)

UPDATE 1
COMMIT

更新冲突与 EvalPlanQual:READ COMMITTED 的写规则 ​

这是 READ COMMITTED 最需要理解透彻的机制。当 UPDATE 发现目标行已被另一个未提交事务修改时,它不会立刻放弃,而是:

text
UPDATE 遇到「行已被并发未提交事务修改」:

1. 阻塞等待对方事务结束
2. 对方 ROLLBACK  → 直接用本方数据更新(无事发生)
3. 对方 COMMIT    → 取出该行的「最新已提交版本」
4. 用最新版本重新检查本语句的 WHERE 条件(EvalPlanQual 重检查)
5. 条件仍满足 → 基于最新版本执行更新
6. 条件不再满足 → 跳过该行(UPDATE 0),不报错
text
会话 A(READ COMMITTED):
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100;
-- 先执行,拿到行锁,未提交

会话 B(同时执行同一条语句):
UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100;
-- B 被阻塞,等待 A

会话 A 提交后:
-- B 用 A 提交后的最新版本重新检查 balance >= 100
-- 若仍满足则继续扣减;若不满足则该行被跳过(UPDATE 0)
text
-- 会话 B 最终输出
UPDATE 1     -- 若 A 提交后余额仍 >= 100
-- 或
UPDATE 0     -- 若 A 提交后余额已 < 100,该行被跳过

这个「重新检查」机制意味着:在 READ COMMITTED 下,一条 UPDATE ... WHERE 语句总是基于「最新已提交数据」判断条件,不会误改已被别人改成不符合条件的行。 但要注意它不保证跨语句的一致性——重检查只针对当前这条语句。

sql
-- 验证「条件写」的收敛性:库存扣减不会超卖
BEGIN;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
-- 若受影响行数为 0,说明库存已空,业务应据此失败或重试
COMMIT;
text
BEGIN
UPDATE 1
COMMIT

丢失更新(lost update):READ COMMITTED 的经典陷阱 ​

「读出来 → 在应用里算好新值 → 写回去」这个模式在 READ COMMITTED 下会丢失更新:

text
会话 A:
BEGIN;
SELECT balance FROM accounts WHERE id = 1;    -- 读到 1000
-- 应用计算:1000 - 100 = 900

会话 B(同时):
BEGIN;
SELECT balance FROM accounts WHERE id = 1;    -- 也读到 1000
-- 应用计算:1000 - 200 = 800
UPDATE accounts SET balance = 800 WHERE id = 1;   -- 提交
COMMIT;

会话 A 继续:
UPDATE accounts SET balance = 900 WHERE id = 1;   -- 覆盖了 B 的 800
COMMIT;
-- 结果:900,B 的扣减 200 被完全抹掉(丢失更新)
text
-- 会话 A 输出
BEGIN
 balance
---------
 1000.00
(1 row)

UPDATE 1
COMMIT
-- 最终 balance = 900.00,业务上丢了 200

注意:这不是 PG「没检测到冲突」。PG 在 A 的 UPDATE 时确实看到了 B 的提交,但 A 写的是一个基于旧值算出的常量 900,重检查只验证 WHERE 条件(这里没有条件),不会帮你重新计算表达式。

三种正确写法:

sql
-- 写法 1(推荐):把计算交给数据库,用相对更新
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
text
BEGIN
UPDATE 1
COMMIT
sql
-- 写法 2:加行锁,让「读—算—写」串行化
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;
text
BEGIN
 balance
---------
 1000.00
(1 row)

UPDATE 1
COMMIT
sql
-- 写法 3:升级到 REPEATABLE READ,让冲突以 40001 暴露出来
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;
text
BEGIN
 balance
---------
 1000.00
(1 row)

ERROR:  could not serialize access due to concurrent update

写法 3 的思路值得牢记:当业务逻辑必须在应用层计算新值时,用更高的隔离级别把「静默丢失」变成「显式冲突」。

幻读:READ COMMITTED 无法防止 ​

幻读(phantom read)指「同一事务内两次执行同一个范围查询,第二次看到了新插入的行」。

text
会话 A(READ COMMITTED):
BEGIN;
SELECT count(*) FROM orders WHERE amount > 1000;   -- 得到 5
-- 会话 B 插入一条 amount = 2000 的订单并提交
SELECT count(*) FROM orders WHERE amount > 1000;   -- 得到 6(多出一行)
COMMIT;
text
-- 会话 A 输出
BEGIN
 count
-------
     5
(1 row)

 count
-------
     6
(1 row)

COMMIT
text
-- 会话 B 在此期间执行
INSERT INTO orders (amount) VALUES (2000.00);
COMMIT;

READ COMMITTED 下幻读是允许的,因为它只承诺「语句级快照」。要防止幻读,需要升级到 REPEATABLE READ(PG 的 RR 能防幻读,这一点比 SQL 标准的最低要求更强)。

值得强调的是:UPDATE 语句在 READ COMMITTED 下对「幻影行」的处理与 SELECT 不同。一条 UPDATE ... WHERE range 会看到语句开始后新提交的行(它用最新快照重新扫描),不会漏掉它们:

sql
-- 会话 A
BEGIN;
UPDATE orders SET reviewed = true WHERE amount > 1000;
-- 此语句执行期间,会话 B 插入并提交了一条 amount = 2000 的订单
-- A 的这条语句仍会看到并更新它
COMMIT;   -- UPDATE 6

用 FOR UPDATE 消除「检查—使用」间隙 ​

在 READ COMMITTED 下,SELECT ... FOR UPDATE 是最直接的加固手段:它读出的同时给行加锁,其它事务想改这些行必须等你结束。

sql
BEGIN;

-- 锁定账户行,后续读到的余额在本事务内不会被别人改动
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;

-- 应用层计算并写回,安全
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

COMMIT;
text
BEGIN
 balance
---------
 1000.00
(1 row)

UPDATE 1
COMMIT

不同锁子句的取舍:

锁子句行为适用
FOR UPDATE排他行锁,阻止其它 FOR UPDATE/FOR NO KEY UPDATE/UPDATE需要修改这些行
FOR NO KEY UPDATE较弱,允许并发 FOR KEY SHARE改非键列,需与外键检查共存
FOR SHARE共享行锁,阻止修改但允许并发共享读只读但要防止被改
FOR KEY SHARE最弱,只阻止键值修改外键引用检查
FOR UPDATE SKIP LOCKED跳过已被锁定的行任务队列消费(不必等待)
sql
-- 任务队列的经典写法:抢任务,跳过别人正在处理的
BEGIN;
SELECT id, payload FROM jobs
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;   -- 返回 id = 1
UPDATE jobs SET status = 'running' WHERE id = ...;
COMMIT;

并发写同一行的两个会话:完整时间线 ​

下面把 READ COMMITTED 的典型并发场景完整走一遍,便于对照理解「阻塞 + 重检查」:

text
时间线(两个会话都是默认的 READ COMMITTED):

时刻  会话 A                              会话 B
----  ----------------------------------  ----------------------------------
t1    BEGIN;                              BEGIN;
t2    UPDATE accounts                     
      SET balance = balance - 100         
      WHERE id = 1;                       
      -- 拿到行锁,未提交                 
t3                                        UPDATE accounts
                                          SET balance = balance - 50
                                          WHERE id = 1;
                                          -- 被阻塞,等待 A
t4    SELECT balance FROM accounts        
      WHERE id = 1;                       
      -- 看到自己未提交的新值(自己的事务可见)
t5    COMMIT;                             
      -- 释放锁                          
t6                                        -- 解除阻塞,取出 A 提交后的最新版本
                                          -- 用最新版本重新检查 WHERE(无附加条件)
                                          -- 基于最新值继续 -50
t7                                        SELECT balance FROM accounts
                                          WHERE id = 1;   -- 看到 850
t8                                        COMMIT;

初始 balance = 1000,两个会话都提交后结果是 850——两个扣减都被正确累计,没有丢失更新。这正是「相对更新(balance = balance - x)」在 READ COMMITTED 下天然安全的原因。

sql
-- 最终验证
SELECT balance FROM accounts WHERE id = 1;
text
 balance
---------
  850.00
(1 row)

设置方式与生效范围 ​

READ COMMITTED 涉及三层配置,优先级从高到低:

层级语法生效范围
事务级SET TRANSACTION ISOLATION LEVEL ...当前事务,必须在任何查询前
BEGIN 时指定BEGIN ISOLATION LEVEL ...当前事务
会话级SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ...当前连接后续所有事务
数据库级ALTER DATABASE db SET default_transaction_isolation = ...该库所有新连接
角色级ALTER ROLE app SET default_transaction_isolation = ...该角色所有新连接
实例级ALTER SYSTEM SET default_transaction_isolation = ...全实例
sql
-- 事务级:必须在事务开始后、任何查询之前设置
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT 1;   -- 查询开始
COMMIT;
text
BEGIN
SET
 ?column?
----------
        1
(1 row)

COMMIT
sql
-- 错误!事务内已有查询后不能再改隔离级别
BEGIN;
SELECT 1;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
text
ERROR:  SET TRANSACTION ISOLATION LEVEL must be called before any query
ini
# postgresql.conf:把默认级别显式写出(虽然默认就是它)
default_transaction_isolation = 'read committed'
sql
-- 会话级修改
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED;
SHOW transaction_isolation;
text
SET
 transaction_isolation
-----------------------
 read committed
(1 row)

适用场景与不适用场景 ​

READ COMMITTED 是「够用且便宜」的默认值,但并非万能:

场景READ COMMITTED 是否合适说明
普通 CRUD、单条语句业务合适语句级原子性足够
相对更新(x = x + n)合适数据库自己完成计算
高并发吞吐优先的系统合适冲突少、无 40001 重试
长报表 / 需要一致快照的统计不合适多条 SELECT 间数据会变
「读—算—写」复合逻辑需加固用 FOR UPDATE 或条件写
跨行不变式(总和、配平)不合适需 REPEATABLE READ / SERIALIZABLE
对账、结算、批量核对不合适需快照一致性
sql
-- 不推荐:READ COMMITTED 下做多表一致性统计
BEGIN;   -- 默认 READ COMMITTED
SELECT sum(balance) FROM accounts;                 -- 时刻 T1 的总额
SELECT sum(amount)  FROM transfers;                -- 时刻 T2 的总额
COMMIT;
-- 两个数来自不同时刻,可能对不上账
text
BEGIN
   sum
-----------
 150000.00
(1 row)

   sum
----------
 149900.00
(1 row)

COMMIT
sql
-- 推荐:需要一致快照时升级到 REPEATABLE READ
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT sum(balance) FROM accounts;
SELECT sum(amount)  FROM transfers;
COMMIT;
text
BEGIN
   sum
-----------
 150000.00
(1 row)

   sum
-----------
 150000.00
(1 row)

COMMIT

与其他隔离级别的对比 ​

对比项READ COMMITTEDREPEATABLE READSERIALIZABLE
快照粒度每条语句整个事务整个事务
不可重复读可能不可能不可能
幻读可能不可能(PG 特性)不可能
丢失更新可能(读—算—写)报错 40001报错 40001
写偏斜可能可能不可能
冲突时行为阻塞 + 重检查,通常成功阻塞 + 报 40001阻塞 + 报 40001
需要应用重试一般不需要需要需要
并发吞吐最高中较低
适用默认、大多数业务一致性读、长查询资金、库存等强一致
sql
-- 同一条语句在两种级别下的冲突表现:
-- READ COMMITTED:阻塞后重检查,通常成功
BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;   -- UPDATE 1
COMMIT;

-- REPEATABLE READ / SERIALIZABLE:若快照后该行被改,报 40001
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
COMMIT;
text
BEGIN
UPDATE 1
COMMIT

BEGIN
ERROR:  could not serialize access due to concurrent update

实战示例 ​

场景:一个「抢购」流程,展示 READ COMMITTED 下如何用「条件写 + 相对更新」安全扣减库存而不超卖。

第 1 步:准备数据

sql
DROP TABLE IF EXISTS flash_orders, stock;

CREATE TABLE stock (
  sku text PRIMARY KEY,
  qty int  NOT NULL CHECK (qty >= 0)
);

CREATE TABLE flash_orders (
  id      bigserial PRIMARY KEY,
  sku     text NOT NULL,
  user_id int  NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO stock (sku, qty) VALUES ('A100', 3);
text
DROP TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 1

第 2 步:条件写扣库存(不会超卖)

sql
BEGIN;
-- 只有 qty > 0 时才会扣减;受影响行数为 0 表示抢购失败
UPDATE stock SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;

-- 若上一步 UPDATE 1,则记录订单;若 UPDATE 0,业务应回滚或跳过
INSERT INTO flash_orders (sku, user_id) VALUES ('A100', 1001);
COMMIT;
text
BEGIN
UPDATE 1
INSERT 0 1
COMMIT

第 3 步:并发争抢,观察最后一个失败

text
会话 1 / 2 / 3(并发执行同一条语句):
UPDATE stock SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;

结果:前两个得到 UPDATE 1,第三个等待后重新检查
      发现 qty 已为 0,条件不满足 → UPDATE 0(抢购失败,不超卖)
text
-- 会话 3 输出
UPDATE 0

第 4 步:验证库存与订单一致

sql
SELECT qty FROM stock WHERE sku = 'A100';
SELECT count(*) AS orders, (SELECT qty FROM stock WHERE sku = 'A100') AS remaining
FROM flash_orders WHERE sku = 'A100';
text
 qty
-----
   1
(1 row)

 orders | remaining
--------+-----------
      1 |         1
(1 row)

初始 3 件,卖出 1 件,剩 1 件——语句级原子性保证了「扣减」与「条件判断」不可分割。

第 5 步:对比错误写法(先读后写)

sql
-- 不推荐:READ COMMITTED 下先读后写会超卖
BEGIN;
SELECT qty FROM stock WHERE sku = 'A100';   -- 读到 1
-- 应用判断 1 > 0,决定下单
UPDATE stock SET qty = qty - 1 WHERE sku = 'A100';   -- 无条件扣减
SELECT qty FROM stock WHERE sku = 'A100';   -- 读到自己的结果
COMMIT;
text
BEGIN
 qty
-----
   1
(1 row)

UPDATE 1
 qty
-----
   0
(1 row)

COMMIT

单看这个会话没问题,但当多个会话并发执行同一段代码时,SELECT 与 UPDATE 之间的窗口会让 qty 被扣成负数——或者因为 UPDATE 无条件而扣穿。这就是「条件写」比「先读后写」更可靠的直接理由。

第 6 步:清理

sql
DROP TABLE flash_orders, stock;
text
DROP TABLE

注意事项 ​

  • PostgreSQL 没有真正的 READ UNCOMMITTED。 请求它只会得到 READ COMMITTED,脏读在任何情况下都不可能发生。错误! 指望用 READ UNCOMMITTED 提升性能(没有任何效果)。
sql
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
SHOW transaction_isolation;
text
BEGIN
 transaction_isolation
-----------------------
 read committed
(1 row)
  • 同一事务内两次 SELECT 结果可能不同。 这是语句级快照的必然结果,不是 bug。错误! 在事务内缓存第一次的读取结果并假设它一直有效。
sql
BEGIN;
SELECT count(*) FROM orders;   -- 第一次
SELECT count(*) FROM orders;   -- 第二次,可能已变
COMMIT;
  • 「读—算—写」在 READ COMMITTED 下会丢失更新。 PG 不会帮你重算表达式,它只重检查 WHERE 条件。不推荐把计算放在应用层再用常量写回。
sql
-- 错误!应用算出 900 再写回,会覆盖并发扣减
UPDATE accounts SET balance = 900 WHERE id = 1;

-- 正确:把计算下推给数据库
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  • 幻读在 READ COMMITTED 下是允许的。 需要「范围查询结果稳定」的场景必须升级隔离级别。错误! 依赖未加锁的范围读做「先查不存在再插入」的防重。
sql
-- 错误!两个事务可能都查到「不存在」然后都插入
BEGIN;
SELECT 1 FROM users WHERE email = 'a@b.c';
INSERT INTO users (email) VALUES ('a@b.c');
COMMIT;

-- 正确:用唯一约束兜底,或加锁
INSERT INTO users (email) VALUES ('a@b.c') ON CONFLICT (email) DO NOTHING;
  • SET TRANSACTION ISOLATION LEVEL 必须在任何查询之前。 一旦执行过 SQL,再改会报 25001。
sql
-- 错误!SET TRANSACTION ISOLATION LEVEL must be called before any query
BEGIN;
SELECT 1;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  • FOR UPDATE 只保护「被锁住的行」。 它不保护范围查询的结果集,也不保护尚未存在的行。不推荐 用 FOR UPDATE 代替真正的隔离级别。
sql
-- 错误认知:以为锁了已存在的行就能防插入
BEGIN;
SELECT * FROM users WHERE email = 'a@b.c' FOR UPDATE;   -- 无行可锁!
INSERT INTO users (email) VALUES ('a@b.c');             -- 仍可能冲突或重复
COMMIT;

最佳实践 ​

  • 把 READ COMMITTED 当作默认值,但显式写出关键事务的隔离级别,让代码意图明确、便于审计。
sql
BEGIN ISOLATION LEVEL READ COMMITTED;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
COMMIT;
  • 用「条件写」替代「先读后写」:把业务判断下推到 WHERE,用受影响行数判断结果。
sql
UPDATE accounts SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- 受影响行数 0 → 余额不足,业务处理失败分支
  • 涉及「读—算—写」时优先加行锁;高并发抢购场景用 FOR UPDATE SKIP LOCKED 避免排队。
sql
BEGIN;
SELECT qty FROM inventory WHERE sku = 'A100' FOR UPDATE;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100';
COMMIT;
  • 需要一致快照的长查询 / 报表用 REPEATABLE READ READ ONLY,而不是在 READ COMMITTED 下拼多条 SELECT。
sql
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT sum(balance) FROM accounts;
SELECT sum(amount)  FROM transfers;
COMMIT;
  • 为无法用锁表达的跨行不变式升级到 SERIALIZABLE,并准备好 40001 重试逻辑。
sql
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM oncall WHERE shift = 'night';
INSERT INTO oncall (doctor) VALUES ('dr-x');
COMMIT;
-- 冲突时捕获 SQLSTATE 40001 并重试

下一节

继续学习:REPEATABLE READ

参考链接 ​