ITPub博客

首页 > 数据库 > Oracle > 定时作业并发重叠的避免方法

定时作业并发重叠的避免方法

原创 Oracle 作者:a960549548 时间:2026-09-17 11:11:29 0 删除 编辑

定时作业的并发重叠是运维中的常见隐患:上一轮作业还没跑完,调度器又启动了下一轮,两个进程同时处理同一批数据,轻则重复劳动,重则相互等待甚至死锁。应用侧的常规做法是往状态表里插一条"运行中"记录作为标志,但这个方案有两个天然缺陷:先插记录再判断之间存在窗口期;进程异常退出后记录残留,需要人工清理。

Oracle 数据库本身其实提供了一个现成的解决方案:DBMS_LOCK 包。它除了提供不同会话可见的命名用户锁,还具备会话断开自动释放的特性,恰好一一化解上述两个缺陷,是实现作业互斥最省事的方式。

一、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)只开放申请和释放,控制锁的使用范围。

二、模拟演示dbms_lock

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:

  1. ALLOCATE_UNIQUE 内部会执行 COMMIT:如果申请锁的代码运行在事务中间,调用它会连带提交当前事务。12.1 起提供了 ALLOCATE_UNIQUE_AUTONOMOUS,用自治事务方式分配句柄,避免这个问题;

  2. 锁名不要以 ORA$ 开头(Oracle 产品保留),建议用"模块名+用途"的命名规则,便于在 DBMS_LOCK_ALLOCATED(名字与锁 ID 的映射表)中排查;

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

  4. 除命名锁外,REQUEST 也支持直接传 0 到 1073741823 的数字 ID,省去 ALLOCATE_UNIQUE 的开销,但可读性差,需要自行规划 ID 不冲突;

  5. 返回码 4 表示本会话已持有该锁,守卫逻辑中应与 0 同等对待,否则重入调用会被误判为失败。

相比自建状态表,DBMS_LOCK 的方案没有窗口期(申请锁是原子操作)、没有残留(会话结束自动释放)、DBA 可通过 V$LOCK 直接观测,几行代码就能让定时作业具备防重入能力。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70013625/viewspace-3182683/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
Oracle ACE,MySQL 8.0 OCP,PMP,拥有OceanBase OBCP,TiDB PCTP,GoldenDB GDCP等多项认证,ITPUB认证专家,微信公众号:DBA之友。

注册时间:2022-02-08

  • 博文量
    132
  • 访问量
    171498