redis单个命令(如hmset、hincrby)本身原子,但组合使用无法保证购物车批量操作原子性,因多命令间存在网络延迟与并发竞态;pipeline仅优化性能,不提供隔离;lua脚本在服务端一次性执行,天然原子,是唯一可靠方案。

为什么直接用 HMSET + HINCRBY 无法保证购物车批量操作原子性?
因为 Redis 的单个命令是原子的,但多个命令组合(比如先查库存、再扣减、再更新购物车)在网络往返中必然存在竞态。哪怕用 PIPELINE 也只是减少延迟,不提供事务隔离——中间可能被其他客户端插入修改。Lua 脚本在服务端一次性执行,天然具备原子性,这才是真正可靠的方案。
常见错误现象:WATCH + MULTI 在高并发下频繁失败重试,吞吐骤降;或业务层自己加分布式锁,引入复杂性和延迟。
- Redis 6.0+ 支持
EVAL和EVALSHA,脚本必须全部在服务端完成,不能调用外部 API 或 sleep - 脚本内不能使用
KEYS、SCAN等阻塞命令,否则影响整个实例 - 所有键名必须通过
KEYS数组传入,不能硬编码,否则Redis Cluster下会报CROSSSLOT错误
购物车批量添加/更新的 Lua 脚本怎么写才安全?
典型场景:用户一次性加入 3 个商品,每个带数量,需校验库存、累加数量、记录最后操作时间。脚本必须把「读-判-写」全包在内,且所有 key 都落在同一 slot(用 {cart:123} 这种哈希标签强制对齐)。
关键点:用 redis.call("HGET", KEYS[1], ARGV[i]) 读原值,用 tonumber() 转数字防 nil,用 math.min() 做库存截断,最后统一 HSET 写回。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local cart_key = KEYS[1]
local stock_keys = KEYS[2] -- "stock:1001", "stock:1002", ...
local item_ids = ARGV[1] -- "1001,1002,1003"
local quantities = ARGV[2] -- "2,1,5"
<p>local ids = redis.call("SMEMBERS", stock_keys) -- 实际应预存可用 sku 列表
local id_list = {}
for str in string.gmatch(item_ids, "[^,]+") do table.insert(id_list, str) end</p><p>local qty_list = {}
for str in string.gmatch(quantities, "[^,]+") do table.insert(qty_list, tonumber(str)) end</p><p>for i, id in ipairs(id_list) do
local stock_key = "stock:" .. id
local stock = tonumber(redis.call("GET", stock_key) or "0")
local cur_qty = tonumber(redis.call("HGET", cart_key, id) or "0")
local add_qty = qty_list[i] or 0
if cur_qty + add_qty > stock then
return {["error"] = "out_of_stock", ["sku"] = id}
end
end</p><p>-- 全部校验通过才写入
for i, id in ipairs(id_list) do
redis.call("HINCRBY", cart_key, id, qty_list[i])
end
redis.call("HSET", cart_key, "updated_at", tonumber(ARGV[3]))
return "ok"</p>
EVALSHA 缓存脚本后,如何避免 NOSCRIPT 报错?
脚本首次用 EVAL 加载后,Redis 会缓存其 SHA1 值;后续用 EVALSHA 调用更快。但若 Redis 重启或脚本被驱逐(maxmemory-policy 设为 allkeys-lru 时可能),EVALSHA 就会返回 NOSCRIPT 错误。
- 客户端必须捕获
NOSCRIPT,自动 fallback 到EVAL重载脚本 - 不要依赖
SCRIPT LOAD提前加载——它不保证持久,且集群模式下需手动分发到各节点 - 推荐做法:启动时用
EVAL执行一次空操作(如return 1),确保脚本已加载;或每次调用前先SCRIPT EXISTS检查
用 HASH 存购物车,为什么别用 EXPIRE 直接设过期?
因为 HSET 不会刷新整个 HASH 的 TTL,只更新字段值。如果用 EXPIRE cart:123 3600,之后每次 HINCRBY 都不会延长过期时间,一小时后整个购物车消失——用户刚加完商品就发现清空了。
- 正确做法:每次写操作后显式调用
EXPIRE cart:123 3600,Lua 脚本里加一行redis.call("EXPIRE", KEYS[1], 3600) - 更优解:用
RedisJSON或Stream存购物车事件,TTL 控制更灵活,但需要 Redis 7.0+ - 注意
EXPIRE返回 0 表示 key 不存在,脚本里要检查,避免静默失败
最易忽略的是哈希标签和 key 分布——没加 {cart:123} 就直接用 cart:123,在集群环境下脚本会因跨 slot 被拒绝,错误信息是 CROSSSLOT Keys in request don't hash to the same slot。










