ioredis重复创建同名键未报错 set加EX参数始终返回OK问题
问题原因
你对Redis SET命令的语义理解有误。SET key value EX 30 本身是默认覆写语义:无论键是否已经存在,只要服务端执行命令成功就会返回OK,不会返回错误或false,这是Redis原生的命令行为,和ioredis客户端无关。
你之前尝试的EXISTS判断、TTL判断方案都存在原子性缺陷,无法实现可靠的互斥锁,在多实例部署、并发触发的场景下必然出现竞态问题。
正确实现调度任务分布式锁的方案
要实现「锁不存在才加锁,加锁失败直接跳过」的互斥逻辑,必须使用Redis SET命令的NX参数,整个加锁操作是原子的,不存在竞态窗口。完整实现代码如下:
import * as redis from '../redis' const redCli = redis.get(); // 锁唯一值,拼接进程ID+时间戳,避免不同实例/不同次执行的锁值冲突 const getLockValue = () => `lock:${process.pid}:${Date.now()}`; // 原子解锁Lua脚本:只有锁值匹配才执行删除,避免误删其他实例持有的锁 const UNLOCK_SCRIPT = ` if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end `; scheduleJob('my-job', '*/05 * * * * *', async () => { const lockKey = 'my-job-lock'; const lockValue = getLockValue(); // NX参数含义:Only set the key if it does not already exist const lockResult = await redCli.set( lockKey, lockValue, 'EX', 30, // 锁过期时间要大于任务正常执行的最大耗时 'NX' ); // 加锁失败,说明锁正被其他执行实例持有,直接返回跳过本次调度 if (lockResult !== 'OK') { console.log('skip current job run: lock is held by another instance'); return; } try { // 放置实际任务执行逻辑 console.log('lock acquired, start running job'); } finally { // 任务执行完成后原子释放锁 await redCli.eval(UNLOCK_SCRIPT, 1, lockKey, lockValue); } });
实现注意事项
- 禁止拆分「判断键是否存在」+「写入键」为两步操作,两步执行之间存在时间窗口,并发场景下会出现多个实例同时拿到锁的问题
- 锁值不能使用固定字符串,必须为每次加锁生成全局唯一标识,防止任务执行超时、锁自动过期后,当前任务误删其他实例新获取的锁
- 解锁操作必须用Lua脚本保证「校验锁值」+「删除键」的原子性,不能直接调用
DEL命令删锁 - 锁过期时间需要设置为大于任务正常执行的最大耗时,避免任务还没执行完锁就提前释放,导致并发执行
内容的提问来源于stack exchange,提问作者PlayHardGoPro
相关产品推荐
相关产品推荐

