🏺 第13章 数据库控制
📄 一句话概括

Oracle 的行级锁是「写的时候自动加、提交时自动放」,你不用管。 要手动锁整行,才写 SELECT ... FOR UPDATE。 最值钱的一条:读不加锁、也不等锁。 所以「有人在改这张表」不会让你的查询卡住,这是 Oracle 并发能力的来源。

📝 本章和前面几章的关系

13.1 讲「一组语句同进退」。这一篇讲「两组语句撞上了怎么办」。 事务解决的是「原子性」,锁解决的是「隔离性」—— ACID 里的 I,具体就是靠锁和 UNDO 实现的。

一、不加锁会撞出哪三类问题

现象 发生了什么 Oracle 怎么防
丢失更新 两人同读一行,后写的盖掉先写的 写时加行锁
脏读 读到别人没提交、后来回滚了的数据 根本不给你读
不可重复读 同一事务里读两次,值变了 语句级快照

「丢失更新」是最常见的一种,也是唯一必须靠锁解决的。 场景:两个人同时打开同一个订单,都读到「余额 100」,各自减 30 再保存, 最后余额是 70 而不是 40。 第二个人写的时候,第一个人的改动就被覆盖了。 Oracle 的解法很直接:第一个人 UPDATE 的那一刻,这一行就被锁住了。 第二个人再来 UPDATE 同一行,就得等—— 等第一个人提交,然后 Oracle 会【重新读一次】这一行,再执行更新。 所以最终是 40,不会丢。 注意最后半句:等完之后不是接着用旧值算,是重新读。 这一点很关键,它让「等锁」变成安全的——否则等到了也没用,值早就变了。

二、为什么 Oracle 读不加锁

大部分数据库读也要加锁(共享锁),所以「有人在写」会挡住「有人要读」。 Oracle 不这么干:读的时候不看当前数据,看 UNDO 里的旧版本。 具体过程:要读的块如果正被别人改着,Oracle 顺着块头找到 UNDO,把改动反向应用一遍, 拼出一个「你该看到的版本」。 这个过程叫【一致性读】,不需要等任何人。 代价是 UNDO 表空间要够大。 如果一条查询要跑半小时,而这半小时里别人一直在改同一批数据,UNDO 就得保留半小时的旧版本。 撑不住的时候报 ORA-01555(snapshot too old)—— 这个错几乎总是「UNDO 太小」或「查询跑太久」,不是锁的问题。

三、锁的种类:先认清四个名字

锁 管什么 什么时候出现
TX(事务锁) 行级,一个事务一把 任何 DML 自动加
TM(表级锁) 表结构别被改 DML 时加 RX 模式
DDL 锁 改表结构时独占 ALTER TABLE 等
闩锁(latch) 保护内存结构,极短 内部使用,看不到

平时最该关心的是 TX 锁,也就是「谁锁了哪一行」。 TM 锁在 DML 时是 RX 模式,意思是「我在改数据,但你可以同时改别的行」,它不阻塞并发的 DML。 真正会把你卡住的是 DDL 锁。 ALTER TABLE ... ADD COLUMN 要拿独占锁,如果这时候有长事务在改这张表,DDL 就得排队—— 表现出来是「加个字段卡了十分钟」,而不是报错。 所以上线改表结构,要么挑没人用的时段,要么先把长事务清掉。

四、手动加锁:SELECT FOR UPDATE

语法四件套: FOR UPDATE(一直等)、FOR UPDATE NOWAIT(不等,直接报错)、 FOR UPDATE WAIT 5(等 5 秒)、FOR UPDATE SKIP LOCKED(跳过被锁的行)。

写法 等不到时 适合什么
FOR UPDATE 一直等 必须改到,等得起
NOWAIT 报 ORA-00054 宁可失败也别卡住
WAIT 5 5 秒后报 ORA-30006 给个上限
SKIP LOCKED 直接跳过这行 多 worker 抢任务队列

SKIP LOCKED 是这四个里最有用的一个,11g 才有。 典型场景:十个进程同时从任务表里取任务。 不用 SKIP LOCKED,十个进程会挤在同一行上排队; 用了之后,每个进程都能拿到不同的行,吞吐量直接翻十倍。 FOR UPDATE 还有个副作用要知道:它会锁住查出来的每一行。 SELECT ... FOR UPDATE 不加 WHERE,就是把整张表锁了。 所以一定要带上能走索引的条件。

五、锁不释放:怎么查、怎么处理

锁不会自己消失,它跟着事务走。 所以「锁住了」几乎总是等于「有人开了事务没提交」。 常见来源:程序里异常分支漏了 COMMIT/ROLLBACK、有人在 SQL*Plus 里改了数据忘了提交、 连接池里的连接被借走没还。 查的思路是两步:先找「谁在等」,再找「谁挡着它」。 V$LOCK 里 BLOCK 列等于 1 的那一行,就是挡人的那个。 V$SESSION 里 BLOCKING_SESSION 直接告诉你挡你的是哪个会话。 处理只有一条路:找到那个会话,让它提交或者回滚。 实在等不了,ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE—— 注意被杀会话的事务会【回滚】,一个跑了半小时的批量操作,回滚可能比执行还久。 所以杀之前先看一眼它改了多少,别把小事搞成大事。

六、动手 Demo:库级锁与行级锁,实测差在哪

这一篇的 Demo 用 SQLite 实测「锁」这件事,然后自己写一个行级锁模型。 为什么要自己写?因为 SQLite 只有库级锁—— 「A 锁了 1 号行,B 改 2 号行也被挡住」,这不是 Oracle 的行为。 所以 Demo 分两段看:前一段是 SQLite 的真实现象(并且和 Oracle 不一样), 后一段是手写的行级锁模型,行为才和 Oracle 对得上。

# 锁:谁持有哪一行、谁在等谁
import os, sqlite3, tempfile
p = os.path.join(tempfile.gettempdir(), "t59.db")
if os.path.exists(p):
    os.remove(p)
a = sqlite3.connect(p, timeout=0)
a.isolation_level = None
a.execute("create table emp (empno int primary key, sal real)")
a.executemany("insert into emp values (?,?)", [(1, 800.0), (2, 1600.0)])

b = sqlite3.connect(p, timeout=0)
b.isolation_level = None
a.execute("begin immediate")      # 拿写锁
a.execute("update emp set sal = 900 where empno = 1")
try:
    b.execute("update emp set sal = 1700 where empno = 2")
    print("B 改成功了")
except sqlite3.OperationalError as e:
    print("B 报错:", e)   # database is locked
a.execute("rollback")

# 行级锁模型:A 锁 1 号,B 锁 2 号(应放行),B 再锁 1 号(应等)
owner = {}
def lock(sess, key):
    if owner.get(key, sess) == sess:
        owner[key] = sess
        return "OK"
    return "WAIT"           # 这一行被别人占着
print(lock("A", 1), lock("B", 2), lock("B", 1))

实跑输出:

--------------------------------------------------------------------
第五十九课:锁——默认行级,写完才放
--------------------------------------------------------------------

  **两个人同时改同一行,数据库得有个办法让他们排队。**
  **这一课先看 Oracle 的规矩(默认行级锁、只有写才锁),**
  **再用 SQLite 实测一次,看「库级锁」和「行级锁」差在哪。**

  一、先看规矩:Oracle 的锁粒度
  --------------------------------------------------------------------
  改一行              只锁这一行,别人能改别的行
  查(SELECT)        不加锁,也不等锁,这是默认行为
  SELECT FOR UPDATE   才真把行锁住,别人改不动
  锁什么时候放        一直持有到 COMMIT 或 ROLLBACK
  **注意第二条:Oracle 的普通查询【不会被写锁挡住】。**
  **别人正在改这一行、还没提交,你照样能读,读到的是改之前的样子(UNDO 里的旧值)。**
  **这是 Oracle 和很多数据库不一样的地方。**

  二、SQLite 实测:它是库级锁
  --------------------------------------------------------------------
  **A 改了 1 号行,没提交。B 现在改 2 号行(不同的行):**
  **报错:database is locked**(耗时 0.0001 秒)
  **改的是 2 号行,被锁住的却是整张表。**
  **SQLite 只有库级写锁,没有行级锁**——**所以这里「A 锁 1 号行」的实际效果是「A 锁整个库」。**
  **Oracle 不会这样:A 改 1 号行时,B 改 2 号行畅通无阻。**

  三、等不到就报错:timeout 是几秒
  --------------------------------------------------------------------
  **timeout 设 1 秒,实际等了 1.19 秒才报错:database is locked**
  **这就是「等锁」,不是「立刻失败」。**
  **Oracle 里这类等待默认可以一直等下去,**
  **所以生产上「卡住不动」往往就是在等锁。**

  四、行级锁该长什么样:自建一个模型
  --------------------------------------------------------------------
  **两条会话,都按 `SELECT ... FOR UPDATE` 的方式锁行:**
  **A 锁 1 号行,B 锁 2 号行,应该都放行;B 再想锁 1 号行,应该等。**
  A 锁 1 号行:OK
  B 锁 2 号行:OK
  B 再锁 1 号行:WAIT
  **B 拿到 WAIT,说明这一行真被 A 锁住了。**
  **现在 A 提交、放锁,B 才能继续:**
  A 提交后 B 重试:OK
  **锁表现在长这样(行号、持有者):[(1, 'B'), (2, 'B')]**

  五、锁不释放,通常就是忘了提交
  --------------------------------------------------------------------
  **R 只读,读到了 [(1,)],没被挡住。**
  **这一点和 Oracle 一样:写锁不挡读。**
  **但 R 要想改这一行,就要等到 W 提交为止。**
  **程序里如果忘了 commit,这个锁会一直挂到连接断开为止。**
  **表现是「系统越用越慢,重启就好」,查半天查不到原因。**
  **遇到这种,第一件事是查 `V$LOCK` 和 `V$SESSION`,看谁在等谁。**

第一段:A 改了 1 号行没提交,B 改 2 号行,直接报 database is locked。 改的是不同的行,却被挡了——因为 SQLite 的写锁是【库级】的:一次只能有一个写事务。 这不是 Oracle 的行为。 Oracle 里 A 改 1 号行,B 改 2 号行,两个人互不相干,都能提交。 这个差异要记住:拿 SQLite 的经验去推 Oracle,关于锁的每一条都会推错。 第二段:timeout 设 1 秒,实际等了 1.2 秒才报错。 这说明「报错」之前是「等待」——锁等待不是拒绝服务,是先排队,排到超时才放弃。 Oracle 里这个等待默认没有上限,所以卡住的时候是「静静地卡住」。 第三段:手写的行级锁模型。 A 锁 1 号、B 锁 2 号都放行,B 再锁 1 号拿到 WAIT—— 这才是 Oracle 的行级锁:锁的单位是行,冲突也只在同一行上。 第四段:忘了提交会怎样。 只读能读到(写锁不挡读,这一点 SQLite 和 Oracle 一样),但写要等到对方提交。 所以「系统变慢」的第一嫌疑人永远是「有事务没提交」。

30 秒实验

  1. 把 timeout 从 1 秒改成 0.1 和 3,各跑一次。 报错时间会跟着变——这就是「等多久才放弃」这件事的直接证据。
  2. 两个连接都开事务,一个只 SELECT,一个 UPDATE 另一行。 看读的那边会不会被挡。 不会,读不加锁。
  3. 在模型里让 A 锁两行、B 锁两行,打印 owner 字典, 看看「锁表」到底长什么样——真库里这叫 V$LOCK。

常见误区

  1. 「查询会被写锁挡住」。 Oracle 不会。读走 UNDO 快照,不等锁。 如果查询真的卡住了,多半是在等 DDL 锁或者 IO,不是等行锁。
  2. 「FOR UPDATE 只锁住查到的那几行数据」。 是, 但别用它锁整表——不带 WHERE 就是全表加锁,一个会话能把整张表按住。
  3. 「锁是数据库的问题,找 DBA 解决」。 锁是应用开出来的。 漏 COMMIT、事务太长、连接池不归还,这三条占了绝大多数。
  4. 「杀掉会话就没事了」。 被杀会话要回滚,大事务回滚可能跑几十分钟,期间锁还在。 杀之前先看它改了多少。
自测三问(点开对答案) 1. Oracle 的行锁什么时候加、什么时候放? —— **执行 DML 时自动加,`COMMIT` 或 `ROLLBACK` 时放。** 不用手写加锁语句。 2. 为什么 Oracle 的读不会被写锁挡住? —— **读的是 UNDO 里的一致性快照,不是当前数据。** 所以不需要等写的人。 3. `FOR UPDATE NOWAIT` 和 `WAIT 5` 差在哪? —— **`NOWAIT` 一撞就报 `ORA-00054`;`WAIT 5` 等 5 秒,等不到报 `ORA-30006`。** 4. 发现有人锁住了一张表,第一步做什么? —— **先查谁在等、谁挡着它**(`V$SESSION.BLOCKING_SESSION`), **再让那个会话提交或回滚。别急着 kill。**

下一篇讲死锁。锁是好事,但两个人各拿一把、又各要对方那把,就死住了。 这一篇的「行级锁」和「等待」,下一篇会变成「环」和「牺牲一个」。 死锁这个题目在面试里出现频率极高,但它真正的价值是:知道怎么用一行排序把它彻底消掉。

相关笔记