Redis里头那些能帮你搞定线程安全的操作,怎么用才靠谱
- 问答
- 2026-01-16 11:01:26
- 2
最核心的一点是,Redis本身是线程安全的,但这不代表你用了Redis,你的整个应用就自动线程安全了,Redis的线程安全指的是它内部处理命令的方式,Redis采用单线程模型(指处理网络请求和数据操作的核心模块),这意味着在任何给定的时刻,只有一个命令会被执行,你完全不用担心多个客户端同时发送命令会导致Redis内部数据状态错乱,比如不会出现一个命令执行到一半,数据被另一个命令修改的“脏读”问题,这个特性是Redis简单高效的重要原因之一,因为它避免了复杂的锁机制带来的开销。(来源:Redis官方文档关于单线程模型的说明)
你的应用程序通常是多线程的,或者有多个独立的进程实例,问题就出在这里:你的业务逻辑的原子性,Redis无法自动保证。 举个例子,一个经典的库存扣减场景:
- 线程A读取某个商品的库存,比如还剩10件。
- 几乎同时,线程B也读取了库存,看到的也是10件。
- 线程A判断库存大于0,决定扣减1件,然后执行命令将库存设置为9。
- 线程B也判断库存(它读到的还是10)大于0,也决定扣减1件,然后执行命令将库存设置为9。
最终结果是,商品被卖出了2件,但库存只减少了1件,这显然错了,问题的根源在于,“判断库存”和“设置库存”这两个操作虽然在Redis端是依次执行的(因为是单线程),但在你的应用逻辑中,它们组合在一起并不是一个原子操作,两个线程的“读”和“写”操作穿插进行了。
搞定线程安全的关键,在于如何让一系列Redis命令在你的业务场景下“像一条命令一样”原子性地执行,以下是几种靠谱的方法:
使用Redis原子命令:最简单直接的法子
Redis提供了很多本身就是原子操作的命令,它们是解决简单竞争场景的首选,因为不需要引入额外的复杂性。
- INCR/DECR命令:对于计数器场景,比如点赞数、阅读量,直接使用
INCR key和DECR key命令,这两个命令将读取、增加/减少、写入三个操作打包成一个原子操作,绝对不会出现并发问题。 - HSETNX/ SETNX命令:用于实现简单的锁或者确保唯一性。
SETNX key value的意思是“如果key不存在则设置它”,如果多个客户端同时对一个key执行SETNX,只有一个会成功(返回1),其他的都会失败(返回0),这可以用来实现一个最简单的分布式锁基础原语,同样,HSETNX是针对哈希字段的。 - 列表(List)和集合(Set)的操作:像
LPUSH/RPOP、SADD、SREM等对列表和集合的增删操作都是原子的,你可以放心地在多线程环境中使用它们来实现消息队列或去重功能。
怎么用才靠谱:优先查看你的业务逻辑能否用单个Redis原子命令实现,如果能,这是最佳选择,性能最好,也最可靠。
使用Lua脚本:处理复杂事务的利器
当你的业务逻辑无法用一个命令完成,需要多个命令且有条件判断时(就像前面提到的库存扣减),Lua脚本是终极解决方案。(来源:Redis官方文档对EVAL命令的介绍)
Redis允许你将一段Lua代码脚本发送到服务器执行。最关键的特性是:整个Lua脚本在执行时会被当作一个单独的、不可中断的命令来执行,在执行脚本期间,不会有其他任何命令被插入执行,这就完美解决了上面库存扣减的问题。
我们用Lua脚本重写库存扣减:
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
redis.call('DECR', KEYS[1])
return 1 -- 扣减成功
else
return 0 -- 库存不足或不存在,扣减失败
end
你的应用程序只需要调用EVAL命令执行这一段脚本,无论有多少个线程同时执行这个脚本,Redis都会让它们排队,每个脚本都会完整地执行“读库存 -> 判断 -> 扣减”这一整套逻辑,然后再执行下一个,这样就保证了库存扣减的绝对准确性。
怎么用才靠谱:
- 将复杂的、需要原子性保证的多步操作都用Lua脚本实现。
- 确保Lua脚本尽量轻量,不要包含耗时的循环或计算,因为执行脚本期间会阻塞其他所有命令,影响Redis的响应性能。
- 可以考虑使用
SCRIPT LOAD命令预先缓存脚本,然后用EVALSHA通过摘要来执行,节省网络带宽。
使用WATCH命令:乐观锁的实现
除了Lua脚本,Redis还提供了一种称为“乐观锁”的机制,通过WATCH命令实现。(来源:Redis官方文档对WATCH命令的介绍)
“乐观锁”的想法是:我不直接加锁阻止别人修改,而是先留意着这个数据,在我准备修改它之前,如果发现它已经被别人动过了,那我就放弃本次修改,重头再来。
具体步骤是:
- 使用
WATCH key监视一个或多个可能被修改的键。 - 在一个Redis事务(
MULTI/EXEC块)中,执行你的命令。 - 当执行
EXEC时,Redis会检查所有被WATCH的键是否自WATCH之后被其他客户端修改过,如果没有任何修改,事务会正常执行;如果至少有一个被修改了,整个事务会被丢弃,返回nil。
还是用库存扣减的例子:
- 线程A和线程B都
WATCH stock_key。 - 它们都开始事务(MULTI),然后
GET stock_key(注意,事务内的GET只是排队,不立即执行)。 - 假设线程A先执行EXEC,Redis会检查stock_key未被修改,于是执行事务:GET,然后DECR,成功。
- 线程B再执行EXEC时,Redis发现stock_key已经被线程A修改过了,于是直接取消线程B的整个事务,返回失败,线程B的业务代码可以捕获到这个失败,然后选择重试整个流程(重新WATCH,重新GET判断,重新DECR)。
怎么用才靠谱:
- 适用于竞争不那么激烈的场景,如果冲突非常频繁,会导致大量重试,性能下降。
- 逻辑上比Lua脚本更复杂一些,需要在客户端处理重试逻辑。
- 它通常和Redis的事务(MULTI/EXEC)一起使用,但要注意,Redis的事务不同于数据库的事务,它不保证原子性(命令可能部分失败),它只是打包一批命令顺序执行,其原子性依赖WATCH来实现。
总结一下
要让Redis帮你搞定线程安全,记住这几点:
- 核心原则:信任Redis内部的单线程模型,但要对你的应用层业务逻辑的原子性负责。
- 首选方案:看看能不能用一个原生的原子命令(如INCR, SETNX)解决问题,这是最优雅的。
- 主力方案:对于复杂的多步操作,毫不犹豫地使用Lua脚本,它是确保强原子性的最强大、最推荐的工具。
- 备选方案:在特定场景下,可以考虑使用WATCH实现的乐观锁,但要清楚其可能的重试开销。
把需要一起执行的多个操作,要么打包成一个Redis命令(用内置命令或Lua脚本),要么通过WATCH机制让它们在被干扰时自动失效重试,这样用Redis才靠谱。

本文由帖慧艳于2026-01-16发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/81752.html
