用Oracle DBMS_LOCK给定时作业加把锁,告别并发重入

Feng 45 阅读 AI技术

两个定时作业进程同时盯上一批数据,轻则白干一遍,重则把下游状态搅乱。运维里这种”上一轮没跑完、下一轮又被调度器拉起来”的重叠,比想象中常见。

常见土办法是往状态表插一条”运行中”记录当标志位。它有两个绕不过去的坑:先查后插之间存在时间窗口,两个会话可能同时通过检查;进程被 kill 或机器宕机后,那条记录会一直挂着,得靠人手动清。

Oracle 自带一个现成方案:DBMS_LOCK。它提供跨会话可见的命名用户锁,并且会话断开时数据库自动释放,正好把上面两个坑一起填了。

三个子程序就够了

作业互斥真正用到的只有 ALLOCATE_UNIQUE、REQUEST、RELEASE。

ALLOCATE_UNIQUE 按锁名分配一把锁并返回句柄;REQUEST 拿句柄去申请,可指定锁模式、等待超时和是否随提交释放;RELEASE 显式放锁。

锁模式常量和普通锁一致,作业互斥用排他模式 X_MODE(值为 6)即可。NL_MODE=1 空模式,SS_MODE=2 子共享,SX_MODE=3 子排他,S_MODE=4 共享,SSX_MODE=5 共享子排他——这套值不用背,知道 X_MODE 是 6 就行。

REQUEST 的返回值要认全:0 成功拿到锁,1 超时没拿到,2 死锁,3 参数错误,4 本会话已持有该锁,5 非法句柄。

这里有个坑:返回 4 不是失败。守卫逻辑里它应该和 0 同等对待,否则同一会话重入时会被误判成抢锁失败,白白跳过一轮作业。

权限方面和多数内置包不同。DBMS_LOCK 的 EXECUTE 默认不授予 PUBLIC,得由 SYS 显式授权。官方还建议再包一层受限的 cover package,只暴露申请和释放两个动作,把锁的使用边界收住。

两个会话实测一遍

先授权:

grant execute on dbms_lock to test;

会话 A 申请名为 JOB_NIGHTLY_CLEANUP 的锁,timeout 传 0,不随提交释放:

declare
l_handle varchar2(128);
l_ret number;
begin
dbms_lock.allocate_unique(‘JOB_NIGHTLY_CLEANUP’, l_handle);
l_ret := dbms_lock.request(l_handle,
lockmode => dbms_lock.x_mode,
timeout => 0,
release_on_commit => false);
dbms_output.put_line(‘request result = ‘ || l_ret);
end;
/

输出 request result = 0,拿到锁。

用户锁在 V$LOCK 里类型是 UL,DBA 可以直接观察:

select sid, type, id1, lmode, ctime
from v$lock
where type = ‘UL’;

命名锁的 ID 从 1073741824 起分配,LMODE 为 6 就是排他。

会话 B 用 TEST 用户另开一个连接,跑同样的代码、同样的锁名,timeout 仍是 0,结果返回 1——超时,因为锁在 A 手里,B 立即失败。把 timeout 改成 10,B 会先等 10 秒再返回 1。

A 释放后,B 重跑申请代码,返回 0,成功接管。

几个容易踩的细节

ALLOCATE_UNIQUE 内部会执行 COMMIT。如果申请锁的代码正跑在事务中间,这一下会连带把当前事务提交掉,业务数据可能被提前落盘。12.1 起提供了 ALLOCATE_UNIQUE_AUTONOMOUS,用自治事务分配句柄,专门绕开这个问题;老版本上只能把申请锁的动作挪到事务之外。

锁名别用 ORA$ 开头,那是 Oracle 产品保留前缀。建议按”模块名+用途”命名,方便在 DBMS_LOCK_ALLOCATED 这张名字到锁 ID 的映射表里排查。

release_on_commit 默认 FALSE,锁一直持有到显式 RELEASE 或会话断开。传 TRUE 则提交或回滚后自动释放,适合事务级串行化。会话异常断开时锁由数据库回收,不会残留。

REQUEST 也支持直接传 0 到 1073741823 的数字 ID,省掉 ALLOCATE_UNIQUE 的开销,但可读性差,排查时对不上号,除命名锁外不建议这么用。

什么时候别用它

DBMS_LOCK 解决的是同一数据库实例内的互斥。跨库、跨实例的作业协调它管不了,那种场景得靠外部协调服务或数据库作业框架本身。

另外它是数据库层的锁资源,长时间持有会占用锁表条目。作业如果动辄跑几小时,最好确认没有别的逻辑在等同一把锁,否则容易把无关会话拖进等待。

和自建状态表比,DBMS_LOCK 没有检查窗口(申请锁是原子操作)、没有残留(会话结束自动释放)、DBA 能从 V$LOCK 直接看到谁持锁。几行代码就能让定时作业具备防重入能力,比维护一张状态表省心得多。

Feng
这位作者很神秘,还没有填写简介。