超时hsy
好,这一步我们在你刚刚那套“100% 并发安全”的最终版之上,只加两件事:
✅ 超时
✅ 安全抢占
❌ 不引入 JVM 锁
❌ 不引入新表(还是 concurrent_ctrl)
❌ 不破坏“异步线程池 + token 释放”这个正确模型
我会给你一套生产级、逻辑闭环、不会出现你之前担心的混乱场景的实现。
一、核心思想(先给结论)
超时 ≠ 直接释放锁
抢占 ≠ 无条件覆盖
必须满足 三要素:
- 锁有唯一 token(任务身份)
- 抢占是 CAS(条件更新)
- 释放必须校验 token
只要这三点在,永远不会出现“任务 A 释放任务 B 的锁”。
二、表结构(在你已有表上最小扩展)
CREATE TABLE concurrent_ctrl (
biz_key VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
expire_time BIGINT NOT NULL
);
说明:
biz_key:如IMPORT_EXPORTtoken:UUID,标识任务expire_time:毫秒时间戳(锁的“租约”)
三、TaskConControl(核心逻辑)
1️⃣ 加锁(含超时 + 抢占)
@Component
public class TaskConControl {
private static final long TIMEOUT_MS = 30 * 60 * 1000L; // 30 分钟
@Autowired
private ConcurrentCtrlDao dao;
public ConcurrentCtrl lock() {
long now = System.currentTimeMillis();
ConcurrentCtrl lock = new ConcurrentCtrl();
lock.setBizKey("IMPORT_EXPORT");
lock.setToken(UUID.randomUUID().toString());
lock.setExpireTime(now + TIMEOUT_MS);
// ① 先尝试直接插入(无锁)
try {
dao.insert(lock);
return lock;
} catch (DuplicateKeyException ignore) {
// 已存在,走下面逻辑
}
// ② 已存在锁,尝试“超时抢占”
ConcurrentCtrl exist = dao.selectForUpdate("IMPORT_EXPORT");
if (exist == null) {
// 理论上极少发生,兜底
dao.insert(lock);
return lock;
}
// ③ 未超时 → 直接失败
if (exist.getExpireTime() > now) {
throw new RuntimeException("已有任务正在执行");
}
// ④ 已超时 → CAS 抢占
int updated = dao.casUpdate(
lock.getBizKey(),
exist.getToken(),
lock.getToken(),
lock.getExpireTime()
);
if (updated == 1) {
return lock;
}
// ⑤ 抢占失败 → 并发下被别人抢走
throw new RuntimeException("已有任务正在执行");
}
/**
* 释放锁(只能释放自己的)
*/
public void unlock(ConcurrentCtrl lock) {
if (lock == null) return;
dao.deleteByBizKeyAndToken(
lock.getBizKey(),
lock.getToken()
);
}
}
2️⃣ DAO(关键 SQL)
select(用于判断是否超时)
SELECT biz_key, token, expire_time
FROM concurrent_ctrl
WHERE biz_key = #{bizKey}
CAS 抢占(安全核心)
UPDATE concurrent_ctrl
SET token = #{newToken},
expire_time = #{newExpireTime}
WHERE biz_key = #{bizKey}
AND token = #{oldToken}
⚠️ 没有 oldToken = token 这一条件,整个方案就是不安全的
释放(带 token)
DELETE FROM concurrent_ctrl
WHERE biz_key = #{bizKey}
AND token = #{token}
四、EndpointImpl(几乎不变)
@Component
public class EndpointImpl {
@Autowired
private TaskConControl taskConControl;
@Autowired
private ExecutorService executorService;
public void exportData() {
ConcurrentCtrl lock;
try {
lock = taskConControl.lock();
} catch (Exception e) {
return;
}
executorService.submit(() -> {
try {
// do biz
} finally {
taskConControl.unlock(lock);
}
});
}
public void importData() {
ConcurrentCtrl lock;
try {
lock = taskConControl.lock();
} catch (Exception e) {
return;
}
executorService.submit(() -> {
try {
// do biz
} finally {
taskConControl.unlock(lock);
}
});
}
}
五、用你最担心的“32 分钟场景”验证一次
场景:
- 任务 A:执行 32 分钟
- 超时:30 分钟
时间线:
T0
A 加锁
token=A, expire=30
T30
B 请求 → 超时 → CAS 抢占
token=B, expire=60
T32
A 执行完 → unlock(A)
DELETE ... WHERE token = A
👉 0 行影响,不会删 B 的锁
✔️ 安全
六、为什么不需要定时任务“强制解锁”
- 锁是 租约
- 超时后自然可被抢占
- 不存在“误释放”
如果你愿意,可以加一个清理任务:
DELETE FROM concurrent_ctrl
WHERE expire_time < now() - 24h
仅做垃圾清理,不影响正确性。
七、这套方案你已经走到哪一步了?
你现在已经实现的是:
| 能力 | 是否具备 |
|---|---|
| 多实例并发控制 | ✅ |
| 异步安全 | ✅ |
| 超时恢复 | ✅ |
| 安全抢占 | ✅ |
| 不误删 | ✅ |
八、最后一句(架构级总结)
这是一个“带租约的分布式互斥锁”
思路和:
- Redis RedLock
- Zookeeper 临时节点
- Quartz Scheduler
是同一类模型
如果你愿意,下一步我可以帮你:
- 🔁 续租机制(长任务心跳)
- 📊 任务状态表 + 可观测
- 🚦 从“拒绝”升级为“排队执行”
- 🧪 并发/极端场景完整推演
你选一个,我继续往下给。

浙公网安备 33010602011771号