不能直接用Redis命令链实现促销逻辑,因为多个命令组合(如先GET再INCR)在并发下非原子,中间状态易被篡改,导致超卖或绕过限购;而Lua脚本在Redis服务端原子执行,将多步条件逻辑(库存扣减、用户校验、标记生成)封装为单次EVAL,彻底规避竞态。

为什么不能直接用 Redis 命令链实现促销逻辑
Redis 命令本身是原子的,但多个命令组合(比如先 GET 再 INCR 再判断)在并发下会出错——中间状态可能被其他客户端篡改。促销规则常涉及「库存扣减 + 用户限购校验 + 订单生成标记」这类多步条件逻辑,靠客户端拼接命令极易超卖或绕过限制。
Lua 脚本在 Redis 服务端原子执行,所有读写都在单次 EVAL 中完成,天然规避竞态。但要注意:EVAL 的脚本不能含阻塞操作(如 SLEEP),也不能调用非纯函数(如 math.random() 在 Redis 7.0+ 默认禁用)。
- 脚本长度建议控制在 1KB 以内,过长会拖慢主循环
- 避免在脚本里做复杂循环(比如遍历上千个 key),Redis 是单线程,卡住就影响所有请求
- 调试时用
redis-cli --eval测试,别直接上生产
如何用 Lua 正确读取并校验用户活动资格
促销常要查「该用户今天是否已参与」「是否在白名单」「是否满足等级门槛」。这些数据通常分散在不同结构中:用户属性存 hash,参与记录用 set 或 sorted set,活动配置放 string 或 hash。Lua 脚本必须一次性读全再决策。
示例:检查用户 uid:123 是否能领取今日优惠券(每人限领 1 张,活动 ID act:2024-spring):
local user_key = KEYS[1] -- "uid:123"
local act_key = KEYS[2] -- "act:2024-spring"
local today = ARGV[1] -- "20240405"
<p>-- 读活动总库存和单人限额
local total_stock = tonumber(redis.call("HGET", act_key, "stock"))
local per_user_limit = tonumber(redis.call("HGET", act_key, "limit_per_user"))</p><p>-- 读用户今日已领取数
local user_today_count = tonumber(redis.call("HGET", user_key .. ":coupon:" .. today, "count")) or 0</p><p>if total_stock <= 0 or user_today_count >= per_user_limit then
return 0 -- 拒绝
end</p><p>-- 原子扣减库存并记录用户行为
redis.call("HINCRBY", act_key, "stock", -1)
redis.call("HINCRBY", user_key .. ":coupon:" .. today, "count", 1)
return 1关键点:HGET 和 HINCRBY 必须用同一连接上下文,所以全部塞进 Lua;KEYS 和 ARGV 传参比硬编码 key 名更安全,也方便复用脚本。
如何避免 Lua 脚本因 key 失效导致逻辑异常
Redis key 过期是被动清理机制,脚本执行时 EXISTS 返回 1 不代表它一定有效——可能刚被后台删除线程扫掉,但脚本仍能读到旧值。促销场景下这会导致「显示有库存,实际扣减失败」。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 对关键资源(如库存)用
GET/HGET后立刻用EXPIRE续期,或改用带 TTL 的结构(如用zset存时间戳) - 不要依赖
TTL返回值做业务判断,它只反映当前剩余秒数,不保证 key 下一秒还在 - 活动配置类 key 建议设永不过期(
PERSIST),靠应用层控制启停,避免脚本读到空配置
比如库存扣减后,顺手刷新活动 key 的过期时间:redis.call("EXPIRE", act_key, 86400),确保后续请求能读到最新状态。
怎样让 Lua 脚本支持动态规则而不用频繁发版
把规则硬编码进 Lua 脚本,每次调整都要运维重载脚本,风险高、效率低。更合理的方式是把可变参数存在 Redis 里,脚本只负责执行逻辑框架。
例如:满减门槛、折扣率、适用商品列表,都存成 hash:
HMSET act:2024-spring rule:min_amount "199" rule:discount_rate "0.85" rule:item_ids "1001,1002,1005"
脚本里用 redis.call("HGETALL", act_key) 拉取整组规则,再用 string.match 或 string.split(需自己实现简单分割)解析。注意:redis.call("HGETALL") 返回的是交替的 field/value 数组,别直接当 table 用。
真正难处理的是「嵌套条件」,比如「新用户首单满 200 减 50,老用户满 300 减 30」——这种建议拆成两个脚本,或用 redis.call("HGET", user_key, "is_new") 查用户属性后再分支,别在 Lua 里写太深的 if-else。
规则越复杂,脚本越难测试和回滚。上线前务必用真实流量镜像压测,尤其关注 EVALSHA 缓存命中率——没缓存的脚本每次都要传输全文,网络开销不小。

















