Redis - 无锁的原子操作:Redis 如何应对并发访问
文章目录

多个客户端同时操作同一个 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)时,才需要分布式锁。
实践建议
- 优先使用内置复合命令,比如
INCR、SETNX、HINCRBY,能用就别自己拼。 - 复杂业务用 Lua 脚本,把多条命令打包成原子操作。
- 脚本要轻量,避免长时间阻塞主线程。
- 缓存脚本,用
SCRIPT LOAD+EVALSHA减少网络开销。 - 集群环境注意 key 分布,用 Hash Tag 保证脚本里的 key 在同一节点。
- 避免把 Redis 当全功能事务库,原子操作擅长的是简单复合逻辑,复杂业务事务该用其他工具。
理解 Redis 的原子操作,关键在于认清它的本质:单线程模型 + 命令排队执行。在这个基础上,无论是单条命令还是 Lua 脚本,都能保证操作的原子性。这套机制简单而有效,是高并发业务里非常顺手的工具。

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


所有评论(0)