在这里插入图片描述

多个客户端同时操作同一个 key,是 Redis 业务里再常见不过的场景。库存扣减、计数器更新、状态变更,都涉及"读-改-写"的复合操作,如果不做并发控制,最终结果可能完全不对。Redis 提供了一套不依赖传统锁的方案——原子操作,通过单线程模型加 Lua 脚本,把复合操作变成一个不可分割的整体。

并发问题的本质

考虑这样一个场景:商品库存初始 100,两个客户端同时要扣减 1。

客户端 A: GET stock → 100  →  SET stock 99
客户端 B: GET stock → 100  →  SET stock 99  ← 错!应该是 98

两个 GET 都拿到了 100,于是各自算出 99 写回去,少扣了 1。这是经典的 lost update 问题。

在这里插入图片描述

要解决这个问题,需要保证"读-改-写"三步对其他客户端来说是原子的:要么全部完成,要么全部没发生。

单命令的原子性

Redis 的单线程模型让所有命令天然串行执行。任何一条 Redis 命令在执行期间,不会被其他命令打断。所以 Redis 的单条命令本身就是原子的

但单条命令往往不够。GET + SET 是两条命令,中间可能插入别的客户端的命令。要么用一条能完成"读改写"的复合命令,要么把多条命令打包成一个原子单元。

内置的复合原子命令

Redis 提供了一批专门的原子命令,把常见的"读改写"场景打包:

命令 作用 原子性
INCR / DECR 整数自增自减
INCRBY / DECRBY 按指定值增减
INCRBYFLOAT 浮点数增减
SETNX key 不存在才设置
GETSET 设置新值并返回旧值
LPUSH / RPOP 列表操作
SADD / SREM 集合增删

库存扣减用 DECR stock 一条命令搞定,不会有并发问题。

但业务逻辑往往更复杂。比如"扣库存的同时记录购买用户",需要两条命令配合。这时候内置命令就不够用了。

Lua 脚本:自定义的原子操作

Redis 内置了 Lua 解释器,允许把多条命令打包成一个脚本,整体作为一个命令执行。脚本执行期间,Redis 不会处理其他客户端的请求

经典的库存扣减 + 用户记录脚本:

local stock = tonumber(redis.call('GET', KEYS[1]))
if stock <= 0 then
    return 0
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1

调用方式:

EVAL "..." 2 stock:1001 buyers:1001 user_8888

脚本里可以包含条件判断、循环、错误处理等复杂逻辑,把它们都变成一个原子操作。

Lua 脚本的注意事项

不要写慢脚本

脚本执行期间整个 Redis 服务被阻塞。一个 100ms 的脚本,等于让所有其他客户端等 100ms。脚本里要避免:

  • 大循环(处理上万条数据的遍历)
  • 大集合操作(SMEMBERS 一个百万元素的 set)
  • 嵌套调用其他慢命令

控制原则:脚本执行时间最好在毫秒级。

用 EVALSHA 减少网络开销

EVAL 每次都要把脚本完整发到服务端。如果脚本很大且高频调用,网络开销不可忽视。EVALSHA 只发送脚本的 SHA1 哈希值,服务端用之前缓存的脚本执行:

SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 返回 sha1: e0e1f9fabfc9d4800c877a703b823ac0578ff831

EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff831 1 mykey

集群下的 key 分布

脚本里所有 KEYS 必须落在同一个节点,否则会报错。可以用 Hash Tag 强制分布:

EVAL "..." 2 {user:1000}:stock {user:1000}:buyers

与传统锁的对比

维度 分布式锁(如 Redis 实现的锁) 原子操作
实现复杂度 高(需处理超时、续约、释放)
性能开销 加锁/解锁额外网络往返 单次调用
锁等待
适用范围 跨多个 Redis key、跨服务调用 仅限 Redis 内的操作
死锁风险 有(需超时机制)

在这里插入图片描述

经验上:能用原子操作就别用锁。锁的复杂度和性能开销都更大。只有当业务逻辑必须跨越 Redis 之外的资源(比如数据库、外部 API)时,才需要分布式锁。

实践建议

  1. 优先使用内置复合命令,比如 INCRSETNXHINCRBY,能用就别自己拼。
  2. 复杂业务用 Lua 脚本,把多条命令打包成原子操作。
  3. 脚本要轻量,避免长时间阻塞主线程。
  4. 缓存脚本,用 SCRIPT LOAD + EVALSHA 减少网络开销。
  5. 集群环境注意 key 分布,用 Hash Tag 保证脚本里的 key 在同一节点。
  6. 避免把 Redis 当全功能事务库,原子操作擅长的是简单复合逻辑,复杂业务事务该用其他工具。

理解 Redis 的原子操作,关键在于认清它的本质:单线程模型 + 命令排队执行。在这个基础上,无论是单条命令还是 Lua 脚本,都能保证操作的原子性。这套机制简单而有效,是高并发业务里非常顺手的工具。

在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐