在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
相关产品推荐
相关产品推荐

