ShedLock S3 抢锁报 409:为什么竞争失败变成 LockException
ShedLock 7.7.0 的 S3v2 provider 把条件写入 409 抛为异常,7.8.0 已处理。区分抢锁、解锁与续锁,保留租期和任务幂等边界。
多个实例同时触发同一个定时任务,一个实例已经在执行,另一个却报 LockException,内部是 S3 的 409 ConditionalRequestConflict。若使用 ShedLock 7.7.0 的 shedlock-provider-s3v2,需要检查条件写入的冲突处理,而不应先把它当成业务方法失败。
常见日志会经过 S3StorageAccessor.replaceObjectMetadata,并包含:
Unexpected error occurred in scheduled task
net.javacrumbs.shedlock.support.LockException
The conditional request cannot succeed due to a conflicting operation against this resource.
Status Code: 409
409 和“没有抢到锁”需要按操作区分
S3 锁通过对象元数据记录持有者与到期时间,写入时带 If-None-Match 或 If-Match。并发条件写入可能返回 412,也可能返回 409;只有明确的这一条路径,才能把冲突与抢锁结果关联起来。
7.7.0 的实现识别 412,却把 409 包装为 LockException。所以本来应跳过任务的竞争者,可能让调度框架记录一次异常。403 权限不足、网络超时、对象元数据损坏等其他错误,不属于这个处理分支,不能统一吞掉。
还要记录日志里的动作。抢锁时没有取得锁,意味着该实例不能执行任务;解锁或续锁时的冲突,则关系到原有锁的后续状态。不能仅凭“另一个实例跑了”认定所有锁操作都已完成。
采用包含正式修正的 provider
ShedLock 7.8.0 已在创建锁和替换元数据的条件写入分支中处理 409,竞争失败返回 false,而不是向外抛出这类 LockException。在项目兼容范围内更新包含该修正的版本,并核对实际运行的 shedlock-provider-s3v2,不要只改一个未被构建使用的版本属性。
Maven 项目可以查看真实依赖树:
./mvnw dependency:tree -Dincludes=net.javacrumbs.shedlock
示例要求工程已有 Maven Wrapper;多模块工程应在使用这个 provider 的模块核对。核心、Spring 集成和 provider 应保持兼容组合,不能把同名 S3 provider 与 s3v2 的结果混看。
这项修正不承诺“409 后自动重试直到成功”。续锁返回失败,不能继续假定原执行者拥有一段新的租期;解锁路径里的条件替换没有成功,也不等于对象中的到期时间已经更新。尤其是设置了很长 lockAtMostFor 的任务,要观察下一轮是否仍被旧锁阻止。
不要为了让任务继续执行而删除共享锁对象,或在捕获所有 LockException 后直接调用业务方法。前者可能破坏仍有效的租约,后者会让失败实例绕过互斥。是否补跑漏掉的触发,应由任务自己的幂等与调度策略决定。
同时检查任务执行与锁的收尾
用测试 bucket 和原来的多实例触发方式验证:竞争失败的实例没有进入业务方法,取得锁的实例能完成任务,结束后的锁元数据及下一轮触发符合配置。还要单独保留一次权限错误等真实失败,确认它没有被当成普通竞争而掩盖。
ShedLock 的租期仍需覆盖任务最长执行时间。任务超过 lockAtMostFor 后可能失去互斥保证,不能用“日志不再有 409”证明任务绝不会重复执行。发布后保留版本与锁状态的观察结果;恢复旧依赖会重新带回原来的冲突处理行为。


