You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 22:33:11