Node.js并发场景下如何正确使用Redis的GET、SET命令?
嘿,这个问题我之前做并发项目时也踩过坑!本质就是竞态条件在搞鬼——多个并发请求同时读取了Redis里的同一个值,各自修改后再写回去,直接把彼此的更新给覆盖了,自然拿不到预期的3000。要解决这个问题,有几种靠谱的方案,根据你的场景选就行:
方案一:用Redis Lua脚本实现原子操作
Redis执行Lua脚本的时候是单线程原子性的——整个脚本的逻辑会一次性执行完,中间不会被其他请求打断。这是处理这类自定义原子操作最推荐的方式,性能也比锁方案好很多。
把你的add函数改成Lua脚本版本就行:
async function add() { // 定义Lua脚本:获取值、加1、存回Redis,最后返回新值 const luaScript = ` local current_num = redis.call('GET', KEYS[1]) current_num = tonumber(current_num) + 1 redis.call('SET', KEYS[1], tostring(current_num)) return current_num `; // 执行脚本,KEYS[1]对应要操作的key(这里是'num') const newNum = await redisClient.eval(luaScript, 1, 'num'); console.log(newNum); return newNum; }
不管多少个并发请求过来,每个脚本的执行都是原子的,完全不会出现中间被打断的情况,最终肯定能得到3000。
方案二:用Redis分布式锁
如果你的更新逻辑特别复杂(比如需要调用外部服务、做复杂计算,没法塞进Lua脚本里),那可以用Redis实现分布式锁,保证同一时间只有一个客户端能执行GET-SET操作。
这里用Redis官方推荐的SET key value NX PX <过期时间>来实现锁(替代老旧的SETNX),既能保证锁的唯一性,还能设置过期时间防止死锁:
async function add() { const lockKey = 'lock:num'; const lockValue = Date.now().toString() + Math.random(); // 用唯一值标记锁的持有者 const lockExpire = 5000; // 锁过期时间,5秒,根据你的操作耗时调整 try { // 获取锁:只有lockKey不存在时才设置成功(NX),同时设置过期时间(PX) const lockAcquired = await new Promise((resolve) => { redisClient.set(lockKey, lockValue, 'NX', 'PX', lockExpire, (err, reply) => { resolve(reply === 'OK'); }); }); if (!lockAcquired) { // 没拿到锁,稍等一下再重试,避免频繁请求Redis await new Promise(resolve => setTimeout(resolve, 10)); return add(); // 递归重试 } // 拿到锁了,执行你的更新逻辑 let numStr = await get('num'); let num = Number(numStr); num++; await set('num', String(num)); console.log(num); return num; } finally { // 释放锁:只有锁的持有者是当前客户端才释放,防止误删其他客户端的锁 const releaseScript = ` if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end `; await redisClient.eval(releaseScript, 1, lockKey, lockValue); } }
注意:锁的过期时间要设置得比你的更新操作耗时久一点,避免锁提前过期导致并发问题;释放锁一定要用Lua脚本原子判断,防止误删别人的锁。
题外话:为什么不用本地锁?
你的代码是在同一个Node进程里跑三个异步函数,用本地锁(比如Promise锁)也能解决,但如果以后你的应用扩展成多进程或者多实例,本地锁就完全没用了。所以还是推荐Redis层面的原子操作或者分布式锁,扩展性更好。
内容的提问来源于stack exchange,提问作者tallguysbb
相关产品推荐
相关产品推荐

