Oracle 19c中使用FOR UPDATE SKIP LOCKED仍出现同一条作业被两个应用实例重复处理的问题求助
这种情况确实挺头疼的——明明用了FOR UPDATE SKIP LOCKED这种悲观锁机制,结果还是出现了重复处理,结合你提到的双应用实例+双Oracle主实例的架构,我来帮你拆解可能的原因和解决思路:
首先明确你的Oracle架构类型
这是最关键的前提,两种架构的问题根源完全不同:
情况1:Oracle是RAC集群(两个节点共享同一个数据库)
RAC的锁是全局同步的,但可能因为查询写法或锁同步细节出问题;
情况2:Oracle是双主复制架构(比如Active-Active Data Guard,两个独立的Primary库双向同步)
这种架构下两个库的锁是完全本地的,应用实例连不同主库时,对方的锁根本看不到,FOR UPDATE SKIP LOCKED完全起不到跨库的锁作用,必然会重复取Job。
如果是RAC集群架构:问题大概率出在查询写法或锁同步窗口
1. 子查询的无锁阶段导致并发冲突
你当前的SQL写法是先通过无锁的子查询筛选前N条Pending作业,再在外层加锁,这就留下了一个致命的时间窗口:
当两个RAC实例几乎同时执行查询时,子查询是无锁的,都能选中同一条Job;之后尝试加锁时,由于RAC实例间的锁信息通过Cache Fusion同步存在极短延迟,两个实例可能都认为该行未被锁定,从而同时加锁成功。
解决办法:把锁逻辑整合到同一个查询块,消除无锁窗口
修改你的原生SQL为:
SELECT * FROM JOBS j WHERE j.STATUS = 'PENDING' AND j.DUE_DATE < SYSTIMESTAMP ORDER BY j.PRIORITY DESC FETCH FIRST :batchSize ROWS ONLY FOR UPDATE SKIP LOCKED
或者用嵌套查询但把锁直接加到子查询中:
SELECT j.* FROM ( SELECT j2.* FROM JOBS j2 WHERE j2.STATUS = 'PENDING' AND j2.DUE_DATE < SYSTIMESTAMP ORDER BY j2.PRIORITY DESC FETCH FIRST :batchSize ROWS ONLY FOR UPDATE SKIP LOCKED ) j
这种写法会在筛选行的同时直接尝试加锁,已经被其他实例锁定的行会被自动跳过,从根源上避免了无锁子查询带来的并发冲突。
2. RAC锁同步的细节优化
即使改了查询写法,RAC的锁同步延迟在极高并发下仍可能有极小概率触发问题,可以做以下优化:
- 创建组合索引缩短查询时间:给
JOBS表的STATUS、DUE_DATE、PRIORITY建组合索引,大幅减少查询执行时间,压缩并发冲突的窗口:CREATE INDEX IDX_JOBS_PENDING_PRIORITY ON JOBS(STATUS, DUE_DATE, PRIORITY DESC); - 检查Oracle补丁:某些早期的19c版本在RAC环境下对
FOR UPDATE SKIP LOCKED的锁同步存在bug,建议升级到最新的RU(Release Update)补丁。
3. JPA事务的细节验证
你的JobService.fetchJobs用了REQUIRES_NEW事务传播是正确的,不过可以再确认两点:
- 开启JPA的SQL日志,观察
UPDATE语句和事务提交的顺序,确保事务在方法结束时确实提交了; - 禁用
JOBS实体的JPA二级缓存(如果开启了),避免从缓存中读取旧数据,确保每次查询都是直接从数据库获取最新状态。
如果是双主复制架构:必须引入全局锁机制
这种架构下两个Primary库的锁完全独立,本地行级锁无法跨库生效,解决思路有三种:
- 切换为RAC架构:如果业务允许,把数据库改成RAC集群,这样锁是全局的,
FOR UPDATE SKIP LOCKED能正常工作; - 引入分布式锁:用Redis或ZooKeeper实现分布式锁,在查询Job之前先尝试获取对应Job的分布式锁,只有拿到锁的实例才能处理。注意设置合理的锁过期时间,避免死锁;
- 分片分配Job:给每个应用实例分配固定的Job分片(比如按Job ID的哈希值分片),或者用数据库的全局序列来分配Job,从根源上避免两个实例同时处理同一条。
验证方法
可以做个简单测试:暂停一个应用实例,手动插入几条Pending的Job,再启动两个实例,观察日志是否还会出现重复;或者用Oracle的V$LOCK视图,在两个实例同时查询时,查看锁的持有情况,确认是否有同一行被两个实例同时锁定。




