首页 > 数据库 > Oracle > 定时作业并发重叠的避免方法
定时作业的并发重叠是运维中的常见隐患:上一轮作业还没跑完,调度器又启动了下一轮,两个进程同时处理同一批数据,轻则重复劳动,重则相互等待甚至死锁。应用侧的常规做法是往状态表里插一条"运行中"记录作为标志,但这个方案有两个天然缺陷:先插记录再判断之间存在窗口期;进程异常退出后记录残留,需要人工清理。
Oracle 数据库本身其实提供了一个现成的解决方案:DBMS_LOCK 包。它除了提供不同会话可见的命名用户锁,还具备会话断开自动释放的特性,恰好一一化解上述两个缺陷,是实现作业互斥最省事的方式。
与作业互斥相关的是三个子程序:
-- 按名字分配一把锁,返回句柄
DBMS_LOCK .ALLOCATE_UNIQUE (
lockname IN VARCHAR2 ,
lockhandle OUT VARCHAR2 ,
expiration_secs IN INTEGER DEFAULT 864000 ) ;
-- 请求锁
DBMS_LOCK .REQUEST (
lockhandle IN VARCHAR2 ,
lockmode IN INTEGER DEFAULT X_MODE ,
timeout IN INTEGER DEFAULT MAXWAIT ,
release_on_commit IN BOOLEAN DEFAULT FALSE )
RETURN INTEGER ;
-- 释放锁
DBMS_LOCK .RELEASE (
lockhandle IN VARCHAR2 )
RETURN INTEGER ;
几个要点:
锁模式。锁模式常量与常规锁一致,作业互斥场景用排他模式即可:
| 常量 | 值 | 含义 |
|---|---|---|
| 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 | 非法的锁句柄 |
权限。与前面几篇介绍的包不同,DBMS_LOCK 的 EXECUTE 权限默认不授予 PUBLIC,需要 SYS 显式授权;官方文档还建议可以再封装一个受限的封面包(cover package)只开放申请和释放,控制锁的使用范围。
1. 授权
SQL > grant execute on dbms_lock to test ;
Grant succeeded.
2. 会话 A 申请锁
SQL > set serveroutput on
SQL > declare
2 l_handle varchar2 ( 128 ) ;
3 l_ret number ;
4 begin
5 dbms_lock .allocate_unique ( 'JOB_NIGHTLY_CLEANUP' , l_handle ) ;
6 l_ret : = dbms_lock .request (l_handle ,
7 lockmode => dbms_lock .x_mode ,
8 timeout => 0 ,
9 release_on_commit => false ) ;
10 dbms_output .put_line ( 'request result = ' || l_ret ) ;
11 end ;
12 /
request result = 0
返回 0,锁申请成功。用户锁在 V$LOCK 中类型为 UL,DBA 可以直接观测:
SQL> select sid, type, id1, lmode, ctime 2 from v$lock where type = 'UL'; SID TY ID1 LMODE CTIME ---------- -- ---------- ---------- ---------- 137 UL 1073741824 6 42
命名锁会被分配 1073741824 起的锁 ID,LMODE 为 6 即排他模式。
3. 会话 B 抢锁(TEST 用户另开会话)
执行同样的代码(锁名相同),timeout 保持 0:
request result = 1
返回 1(超时):锁被会话 A 持有,会话 B 立即失败返回。把 timeout 改成 10,则会先等待 10 秒再返回 1。
4. 会话 A 释放,会话 B 重试
SQL > declare
2 l_handle varchar2 ( 128 ) ;
3 begin
4 dbms_lock .allocate_unique ( 'JOB_NIGHTLY_CLEANUP' , l_handle ) ;
5 dbms_output .put_line ( 'release result = ' || dbms_lock .release (l_handle )) ;
6 end ;
7 /
release result = 0
释放后再到会话 B 执行第 2 步的申请代码,返回 0,获得锁。
tips:
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 的开销,但可读性差,需要自行规划 ID 不冲突;
返回码 4 表示本会话已持有该锁,守卫逻辑中应与 0 同等对待,否则重入调用会被误判为失败。
相比自建状态表,DBMS_LOCK 的方案没有窗口期(申请锁是原子操作)、没有残留(会话结束自动释放)、DBA 可通过 V$LOCK 直接观测,几行代码就能让定时作业具备防重入能力。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70013625/viewspace-3182683/ ,如需转载,请注明出处,否则将追究法律责任。