资讯详情 ORACLE 关于CURSOR中的FOR UPDATE关键字:从语法到锁行为的完整拆解
📅 2026/10/10 20:19:59
1. 从一段 PL/SQL 说起FOR UPDATE 到底锁了什么先看一段很多人第一次写游标更新时都会写出的代码declare cursor cur_emp is select empno, ename, job from emp where empno 7369 for update of ename; begin for r in cur_emp loop update emp set ename LHG where current of cur_emp; end loop; end; /问题通常出在for update of ename这一句上。很多人第一反应是这里写了ename是不是只锁ename这一列如果改成for update of job锁的范围会变吗答案是不会。Oracle 的行级锁最小粒度就是行不存在“列锁”这种东西。OF 列名只是语法上的占位写ename、job、empno效果完全一样锁住的都是emp表中被WHERE命中的那一整行。真正决定锁范围的是SELECT的过滤条件和涉及的表而不是OF后面跟的字段。那OF到底有什么用它有两个实际意义一是让 SQL 读起来有语义告诉后来维护的人“我打算更新这一列”二是在多表关联的游标里OF可以指定对哪张表加锁。比如from dept a, emp b ... for update of a.dname就只锁dept的行不锁emp。这一点在关联更新场景里很关键写错了会把不该锁的表也锁上。再往下就是锁的生命周期。行锁从游标OPEN或者FOR循环第一次取数开始持有直到事务COMMIT或ROLLBACK才释放而不是游标CLOSE就释放。这是新手最容易踩的坑以为循环结束、游标关闭锁就没了结果另一个会话一直卡着。实际上只要事务没提交锁就一直挂着。理解了这三点——锁的是行不是列、OF用于指定表、锁随事务结束——后面NOWAIT、SKIP LOCKED、WAIT n的差异就很好理解了。这篇就围绕 Oracle 游标FOR UPDATE的语法位置、锁粒度和等待行为给出一套可以在本地库直接复现的脚本把行锁范围亲手验证一遍。2. 本地复现前的准备建表、造数据与 TaoToken 接入要观察锁行为至少需要两个会话同时操作同一批数据。我一般用一个会话跑游标、另一个会话去改同一行看它到底是等待还是立刻报错。下面先把环境搭好。2.1 建一张可反复实验的表-- 建表 create table emp_test ( empno number(4) primary key, ename varchar2(20), job varchar2(20), deptno number(2) ); -- 造数据 insert into emp_test values (7369, SMITH, CLERK, 20); insert into emp_test values (7499, ALLEN, SALESMAN, 30); insert into emp_test values (7521, WARD, SALESMAN, 30); commit;数据量不用大三行足够。关键是empno做主键方便后面用ROWID或主键定位。2.2 观察锁的视图Oracle 里看行锁主要靠v$lock和v$session关联select s.sid, s.serial#, s.username, l.type, l.lmode, l.request, l.id1, l.id2 from v$lock l join v$session s on l.sid s.sid where l.type TX;type TX就是事务行锁。lmode是当前持有模式request是正在申请的模式。如果某个会话request 0说明它在等锁。再配合v$session的blocking_session字段能直接看出谁堵了谁select sid, blocking_session, event, seconds_in_wait from v$session where blocking_session is not null;2.3 关于 TaoToken 的接入位置如果你习惯在本地用 AI 辅助写 PL/SQL、排查报错可以把模型接进编辑器或命令行工具。TaoToken 提供统一的 API 入口Base URL 是https://taotoken.net/apiKey 在控制台生成。以 Claude Code 这类工具为例配置通常落在settings.json或环境变量里三件套是 Base URL、API Key、Model ID{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: 你的API Key, ANTHROPIC_MODEL: claude-sonnet-4-5 } }需要说明的是AI 工具在这里的角色是帮你解释报错、生成测试脚本锁行为的验证仍然要在真实 Oracle 库里跑。Key 的获取和模型列表可以在控制台和文档里查接入文档地址是https://taotoken.net/docAPI Key 管理在https://taotoken.net/api-keys。把工具配好之后遇到ORA-00054这类报错可以直接把错误码贴给模型让它给出排查方向比翻文档快。环境准备好下面进入正题三种等待写法怎么验证。3. 可复制配置NOWAIT / SKIP LOCKED / WAIT n 三种写法这一节给出可以直接粘贴运行的脚本。为了观察等待行为每个实验都需要两个会话会话 A 持有锁不提交会话 B 尝试加锁看它的反应。3.1 基础写法FOR UPDATE 默认无限等待会话 A-- 会话 A declare cursor cur_emp is select empno, ename from emp_test where empno 7369 for update; begin for r in cur_emp loop dbms_output.put_line(A locked: || r.ename); -- 故意不提交保持锁 dbms_lock.sleep(60); end loop; end; /会话 B 在 A 还没提交时执行-- 会话 B update emp_test set ename B_UPDATE where empno 7369;B 会一直挂住直到 A 提交或回滚。这就是默认行为无限等待。如果 A 的程序因为异常没提交也没回滚B 就永远等下去死锁风险就是这么来的。3.2 NOWAIT拿不到锁立刻报错会话 A 同上。会话 B 改成-- 会话 B declare cursor cur_emp is select empno, ename from emp_test where empno 7369 for update nowait; begin for r in cur_emp loop null; end loop; end; /这次 B 不会等直接抛ORA-00054: resource busy and acquire with NOWAIT specified。注意一个语法细节NOWAIT必须跟在FOR UPDATE后面如果同时要写OF顺序是FOR UPDATE OF ename NOWAIT。单独写FOR UPDATE NOWAIT也合法此时锁的是SELECT涉及的所有表。3.3 WAIT n等指定秒数后放弃-- 会话 B declare cursor cur_emp is select empno, ename from emp_test where empno 7369 for update wait 5; begin for r in cur_emp loop null; end loop; end; /B 会等 5 秒5 秒内 A 提交了就能拿到锁继续超时则报ORA-30006: resource busy; acquire with WAIT timeout expired。WAIT n适合那种“可以等一会儿但不想无限等”的场景比NOWAIT温和比默认等待可控。3.4 SKIP LOCKED跳过被锁的行SKIP LOCKED是 11g 之后常用的写法典型场景是任务队列多个消费者并发取任务谁取到算谁的取不到的直接跳过不互相阻塞。-- 会话 B declare cursor cur_emp is select empno, ename from emp_test where deptno 30 for update skip locked; begin for r in cur_emp loop dbms_output.put_line(B got: || r.empno); end loop; end; /如果会话 A 锁住了empno 7369deptno 20B 查 deptno 30 不受影响如果 A 锁的是 deptno 30 里的某一行B 会跳过那一行取到其余未被锁的行而不是整体等待。三种写法的对照写法拿不到锁时行为典型报错适用场景FOR UPDATE无限等待无一直挂确定锁很快释放FOR UPDATE NOWAIT立即返回ORA-00054不想等快速失败FOR UPDATE WAIT n等 n 秒ORA-30006可容忍短暂等待FOR UPDATE SKIP LOCKED跳过该行无队列、并发取任务3.5 用 ROWID 替代 WHERE CURRENT OF原代码里还提到一种写法把ROWID查出来更新时用where rowid :v_rowid。这在复杂程序里更灵活因为WHERE CURRENT OF只能配合游标使用而ROWID可以跨过程传递。declare cursor cur_emp is select a.deptno, a.dname, a.rowid rowid_dept, b.rowid rowid_emp from dept_test a, emp_test b where b.empno 7369 and a.deptno b.deptno for update nowait; v_deptno dept_test.deptno%type; v_dname dept_test.dname%type; v_rowid_dept rowid; v_rowid_emp rowid; begin open cur_emp; loop fetch cur_emp into v_deptno, v_dname, v_rowid_dept, v_rowid_emp; exit when cur_emp%notfound; update dept_test set dname abc where rowid v_rowid_dept; update emp_test set ename frank where rowid v_rowid_emp; end loop; close cur_emp; commit; exception when others then rollback; raise; end; /注意这里FOR UPDATE NOWAIT没有写OF锁的是dept_test和emp_test两张表命中的行。如果只想锁其中一张就写FOR UPDATE OF a.dname NOWAIT。4. 验证请求与成功结果亲手确认行锁范围光看语法不够得跑一遍确认锁到底加在哪。下面这套步骤可以在本地库完整复现。4.1 验证“锁的是行不是列”会话 A 执行-- 会话 A只锁 empno 7369 select empno, ename from emp_test where empno 7369 for update of ename;不提交。会话 B 尝试更新同一行的另一列-- 会话 B update emp_test set job MANAGER where empno 7369;B 会挂住。这说明虽然 A 写的是OF ename但 B 改job一样被挡。结论OF后面的列名不影响锁范围锁的是整行。再让 B 改另一行-- 会话 B update emp_test set job MANAGER where empno 7499;这次 B 立刻成功。说明锁只覆盖WHERE命中的行没命中的行不受影响。4.2 验证锁随事务结束释放会话 A 执行for update后不提交会话 B 挂住。此时在第三个会话查select sid, blocking_session, event, seconds_in_wait from v$session where blocking_session is not null;能看到 B 的blocking_session指向 A 的 sidevent是enq: TX - row lock contention。然后让 A 执行commit;B 立刻恢复执行。这验证了锁的释放点是COMMIT/ROLLBACK不是游标CLOSE。4.3 验证 NOWAIT 的报错会话 A 持锁不提交会话 B 执行declare cursor c is select empno from emp_test where empno 7369 for update nowait; begin for r in c loop null; end loop; end; /预期输出ORA-00054: resource busy and acquire with NOWAIT specified如果没报错反而成功了检查 A 是不是已经提交了或者 A 锁的行和 B 查的行不是同一行。4.4 验证 SKIP LOCKED 的跳过行为会话 A 锁住empno 7369-- 会话 A select empno from emp_test where empno 7369 for update;不提交。会话 B 执行-- 会话 B declare cursor c is select empno from emp_test where empno in (7369, 7499, 7521) for update skip locked; begin for r in c loop dbms_output.put_line(got: || r.empno); end loop; end; /预期 B 只输出 7499 和 7521跳过被 A 锁住的 7369且不等待。这就是队列消费的典型行为。4.5 用 AI 辅助解读报错跑这些实验时报错信息可以直接丢给接入的模型。比如把ORA-00054和你的游标代码一起贴过去让它分析是哪个会话持锁、该怎么改。TaoToken 的模型对话入口在https://taotoken.net/api配合文档里的接入说明几分钟就能配好。模型能帮你快速定位是NOWAIT用错位置还是事务没提交但最终的锁验证还是以v$lock查询为准。5. 本篇常见错排查ORA-00054、ORA-30006 与死锁实验过程中最容易撞上的几个报错这里逐个拆。5.1 ORA-00054 resource busy and acquire with NOWAIT specified完整报错ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired原因目标行已被其他会话锁定且你用了NOWAIT。排查步骤先查谁在持锁select s.sid, s.serial#, s.username, s.status, l.type, l.lmode, l.request from v$lock l join v$session s on l.sid s.sid where l.type TX and l.request 0;找到持锁会话后确认它是不是忘了提交。如果是测试环境可以alter system kill session sid,serial#;强制断开但生产环境要谨慎优先让持锁方提交或回滚。5.2 ORA-30006 resource busy; acquire with WAIT timeout expiredORA-30006: resource busy; acquire with WAIT timeout expired这是FOR UPDATE WAIT n超时。说明 n 秒内锁没释放。处理方式和ORA-00054类似区别是你可以调大 n或者改用SKIP LOCKED跳过。如果频繁超时说明持锁事务太长应该检查业务逻辑是不是把不该放事务里的操作比如远程调用、文件 IO塞进了游标循环。5.3 死锁ORA-00060两个会话互相等对方持有的锁Oracle 检测到后会让其中一个回滚报ORA-00060: deadlock detected while waiting for resource死锁的根因通常是加锁顺序不一致。比如会话 A 先锁 emp 再锁 dept会话 B 先锁 dept 再锁 emp两边各持一把等对方。预防办法一是统一加锁顺序所有程序都按同一张表顺序加锁二是尽量用NOWAIT或WAIT n避免无限等待三是事务里尽早提交缩短持锁时间四是EXCEPTION里必须有ROLLBACK异常时释放锁。5.4 关于 local proxy failed 与 401如果你在配置 AI 工具时遇到local proxy failed或401这通常和数据库无关是接入配置问题。401一般是 API Key 无效或没带上检查ANTHROPIC_AUTH_TOKEN是否填对local proxy failed多是 Base URL 写错或网络不通确认填的是https://taotoken.net/api而不是首页地址。这类问题在接入文档https://taotoken.net/doc里有对应说明Key 在https://taotoken.net/api-keys重新生成即可。5.5 游标里忘写 COMMIT 的后果这是最隐蔽的坑。程序跑完循环、CLOSE游标看起来一切正常但锁没释放。下一个会话来操作同一行就挂住。排查时先看v$lock里有没有TX锁长期存在再看对应会话在跑什么。养成习惯游标更新程序结尾必须有COMMITEXCEPTION里必须有ROLLBACK。6. 把 FOR UPDATE 用稳的几条实操建议跑完上面的实验几条经验可以直接落到日常开发里。NOWAIT尽量跟FOR UPDATE一起用尤其是交互式或高并发场景宁可快速失败也不要无限挂起。OF后面的列名虽然不影响锁范围但在多表关联时用来指定锁哪张表别省。WHERE CURRENT OF适合简单游标复杂程序里用ROWID更灵活可以跨过程传递、精确定位。COMMIT必须出现在程序结尾EXCEPTION里的ROLLBACK是最基本的兜底缺了这两样死锁迟早找上门。队列类场景优先考虑SKIP LOCKED让多个消费者各取各的不互相阻塞。如果业务能容忍短暂等待WAIT n比默认无限等待安全得多。最后锁行为一定要在真实库上验证别只靠文档。v$lock和v$session是你最好的朋友遇到等待先查blocking_session顺着链条找到持锁方问题基本就定位了。把上面那套建表和验证脚本存成自己的测试用例下次遇到锁相关报错直接跑一遍对照比翻手册快得多。