真正有效的防撞方案是分布式锁与信号量分层协作:分布式锁控制批次全局准入,信号量限制单实例内子任务并发数。需规避嵌套死锁、设置超时与自动兜底,并加强可观测性。

在批处理场景中,单纯用 Java Semaphore 或单纯用 DistributedLock 都无法完整解决“多实例并发撞车”问题:Semaphore 只管本进程内线程数,跨 JVM 无效;DistributedLock 虽能跨节点互斥,但若不加并发数控制,又可能把整批任务串行化,吞吐骤降。真正有效的防撞方案,是让两者分层协作——分布式锁管“谁可以进批处理入口”,信号量管“同一入口内最多跑几个子任务”。
分层职责:锁定入口 + 限流执行
批处理常有“一个批次由多个子任务组成”的结构(如:1000 条订单分 10 个线程并行处理)。此时需两道防线:
-
DistributedLock 管全局批次准入:每个批次用唯一 key(如
batch:20260607:order_sync)加锁,确保同一时间仅有一个服务实例启动该批次,避免重复调度、重复拉取、重复写库。 -
Semaphore 控制单实例内并发粒度:获取批次锁后,在本 JVM 内用
Semaphore(5)限制最多 5 个线程并行处理子任务,防止数据库连接池打满、下游接口被压垮、内存溢出等本地资源瓶颈。
必须规避的嵌套死锁陷阱
常见错误是在已持 DistributedLock 的业务块里,再调用另一个分布式锁或跨服务信号量(如调用下游系统的限流 API),极易引发 AB-BA 式分布式死锁。正确做法是:
- 所有需协调的资源(含本地 Semaphore 和远程分布式锁)必须提前声明,统一编号、升序申请;例如定义
LOCK_BATCH=101、SEMAPHORE_DB=202,只允许先 lock batch 再 acquire semaphore,禁止反向或动态嵌套。 - 本地 Semaphore 获取必须设超时(如
tryAcquire(3, TimeUnit.SECONDS)),失败立即释放已持有的 DistributedLock 并退避重试,不阻塞整个批次入口。 - 禁止在
acquire()后的临界区内发起任何可能触发新锁/信号量的远程调用(如 RPC、HTTP、MQ 发送),这类操作应前置或后置到临界区外。
自动兜底与可观测性设计
生产环境不能依赖人工干预,需内置防御机制:
- DistributedLock 必须设置合理过期时间(如 30 分钟),且业务执行完必须显式 unlock;建议用 try-with-resources 封装,或注册 JVM shutdown hook 做最终保障。
- Semaphore 的 permit 数应可动态配置(如从 Apollo/Nacos 加载),便于上线后根据 DB 负载、下游容量实时调优,无需重启服务。
- 关键路径埋点:记录批次锁获取耗时、Semaphore 等待队列长度、permit 使用率,接入 Prometheus+Grafana,当等待线程 >3 或 acquire 超时率 >5%,自动告警。
代码结构示意(非完整实现)
以下为逻辑骨架,强调职责分离与安全边界:
// 1. 全局批次准入(分布式锁)
if (!distributedLock.tryLock("batch:" + batchId, 10, TimeUnit.SECONDS)) {
log.warn("Batch {} rejected: lock acquired by another instance", batchId);
return;
}
try {
// 2. 本机并发控制(Semaphore)
if (!semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
log.warn("Batch {} rejected: local concurrency limit reached", batchId);
return;
}
try {
// 3. 执行实际批处理逻辑(无锁、无信号量嵌套)
processBatchItems(batchId);
} finally {
semaphore.release(); // 确保释放
}
} finally {
distributedLock.unlock(); // 确保释放
}Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











