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

在Express.js中同步Redis读写操作以避免数据覆盖

解决Express.js中Redis读写的竞态条件问题

你的代码中,GET和SET是两个独立的Redis命令,当多个请求几乎同时到达时,第一个请求还没完成SET操作,第二个请求就已经执行了GET,导致两者拿到相同的旧值,最终覆盖彼此的修改——这就是典型的竞态条件。要解决这个问题,核心是把"读-改-写"的操作变成原子性的,或者通过锁机制保证同一时间只有一个请求能操作指定的Redis键。

方案1:使用Redis原子Lua脚本(推荐)

Redis支持执行Lua脚本,脚本内的所有命令会作为一个原子操作执行,不会被其他客户端的命令打断。把你的"读-改-写"逻辑放到Lua脚本里,就能从根源避免竞态条件。

示例代码:

const express = require('express');
const redis = require('redis');

const app = express();
const client = redis.createClient();

// 定义Lua脚本:读取key值、处理、写回
const updateScript = `
local key = KEYS[1]
local current = redis.call('GET', key)
-- 替换这里的逻辑为你的业务处理
local manipulated = current and tonumber(current) + 1 or 1
redis.call('SET', key, manipulated)
return manipulated
`;

app.get('/data', async (req, res) => {
  const key = req.query.key;
  if (!key) {
    return res.status(400).send('Key参数必填');
  }

  try {
    // 执行Lua脚本,KEYS[1]对应传入的key
    const result = await client.eval(updateScript, {
      keys: [key]
    });
    res.send(`数据已更新为: ${result}`);
  } catch (err) {
    res.status(500).send('Redis操作失败');
  }
});

app.listen(3000, () => {
  console.log('服务运行在端口3000');
});

优势:性能最高,不需要额外的锁逻辑,Redis原生支持,适合绝大多数场景。

方案2:单进程内存锁(适合单实例Express)

如果你的Express服务是单进程运行的,可以用内存中的锁队列,保证同一时间只有一个请求处理指定的Redis键。

示例代码:

const express = require('express');
const redis = require('redis');

const app = express();
const client = redis.createClient();

// 维护每个key的锁状态
const keyLocks = new Map();

// 封装带锁的key处理函数
async function handleKeyOperation(key) {
  // 如果当前key有锁,等待锁释放
  while (keyLocks.has(key)) {
    await new Promise(resolve => setTimeout(resolve, 10));
  }

  try {
    keyLocks.set(key, true);
    // 执行读-改-写
    const data = await client.get(key);
    const manipulatedData = data ? parseInt(data) + 1 : 1;
    await client.set(key, manipulatedData);
    return manipulatedData;
  } finally {
    // 释放锁
    keyLocks.delete(key);
  }
}

app.get('/data', async (req, res) => {
  const key = req.query.key;
  if (!key) {
    return res.status(400).send('Key参数必填');
  }

  try {
    const result = await handleKeyOperation(key);
    res.send(`数据已更新为: ${result}`);
  } catch (err) {
    res.status(500).send('Redis操作失败');
  }
});

app.listen(3000, () => {
  console.log('服务运行在端口3000');
});

优势:实现简单,不需要依赖Redis的额外功能;局限:仅适用于单进程服务,多进程/多实例部署时无效。

方案3:Redis分布式锁(适合多进程/多实例场景)

如果你的Express服务部署了多个实例,或者用了多进程模式,内存锁就无法跨进程生效,这时候需要用Redis实现分布式锁。

示例代码:

const express = require('express');
const redis = require('redis');

const app = express();
const client = redis.createClient();

// 获取分布式锁
async function acquireLock(key, lockTimeout = 5000) {
  const lockKey = `lock:${key}`;
  const expireTime = Date.now() + lockTimeout + 1;
  // SETNX:仅当key不存在时设置,同时设置过期时间避免死锁
  const isAcquired = await client.set(lockKey, expireTime, {
    NX: true,
    PX: lockTimeout
  });

  if (isAcquired) return expireTime;

  // 检查锁是否过期,尝试抢占过期锁
  const currentLockExpire = await client.get(lockKey);
  if (currentLockExpire && parseInt(currentLockExpire) < Date.now()) {
    const oldExpireTime = await client.getSet(lockKey, expireTime);
    if (oldExpireTime === currentLockExpire) {
      return expireTime;
    }
  }
  return null;
}

// 释放分布式锁
async function releaseLock(key, expireTime) {
  const lockKey = `lock:${key}`;
  const currentExpire = await client.get(lockKey);
  if (currentExpire === expireTime.toString()) {
    await client.del(lockKey);
  }
}

app.get('/data', async (req, res) => {
  const key = req.query.key;
  if (!key) {
    return res.status(400).send('Key参数必填');
  }

  let lockExpireTime = null;
  try {
    // 最多重试3次获取锁
    let retries = 3;
    while (retries > 0 && !lockExpireTime) {
      lockExpireTime = await acquireLock(key);
      if (!lockExpireTime) {
        await new Promise(resolve => setTimeout(resolve, 100));
        retries--;
      }
    }

    if (!lockExpireTime) {
      return res.status(503).send('请求过于频繁,请稍后重试');
    }

    // 执行读-改-写
    const data = await client.get(key);
    const manipulatedData = data ? parseInt(data) + 1 : 1;
    await client.set(key, manipulatedData);

    res.send(`数据已更新为: ${manipulatedData}`);
  } catch (err) {
    res.status(500).send('Redis操作失败');
  } finally {
    // 释放锁
    if (lockExpireTime) {
      await releaseLock(key, lockExpireTime);
    }
  }
});

app.listen(3000, () => {
  console.log('服务运行在端口3000');
});

优势:支持多进程/多实例场景,保证跨服务的操作同步;注意:要处理锁过期、重试等边界情况,避免死锁或锁抢占失败。


内容的提问来源于stack exchange,提问作者Abhishek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 12:05:59