You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Oracle 19c中使用FOR UPDATE SKIP LOCKED仍出现同一条作业被两个应用实例重复处理的问题求助

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表的STATUSDUE_DATEPRIORITY建组合索引,大幅减少查询执行时间,压缩并发冲突的窗口:
    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库的锁完全独立,本地行级锁无法跨库生效,解决思路有三种:

  1. 切换为RAC架构:如果业务允许,把数据库改成RAC集群,这样锁是全局的,FOR UPDATE SKIP LOCKED能正常工作;
  2. 引入分布式锁:用Redis或ZooKeeper实现分布式锁,在查询Job之前先尝试获取对应Job的分布式锁,只有拿到锁的实例才能处理。注意设置合理的锁过期时间,避免死锁;
  3. 分片分配Job:给每个应用实例分配固定的Job分片(比如按Job ID的哈希值分片),或者用数据库的全局序列来分配Job,从根源上避免两个实例同时处理同一条。

验证方法

可以做个简单测试:暂停一个应用实例,手动插入几条Pending的Job,再启动两个实例,观察日志是否还会出现重复;或者用Oracle的V$LOCK视图,在两个实例同时查询时,查看锁的持有情况,确认是否有同一行被两个实例同时锁定。

火山引擎 最新活动