lpush与expire分属两个独立命令,执行间可能因崩溃或网络中断导致仅入队未设过期,破坏原子性;必须用lua脚本封装,利用redis单线程特性确保“推入+设ttl”整体不可分割。

为什么直接用 LPUSH + EXPIRE 无法保证原子性?
Redis 的 LPUSH 和 EXPIRE 是两个独立命令,中间可能被其他客户端中断或失败。比如你刚 LPUSH 进一个元素,还没来得及 EXPIRE,进程崩溃或网络断开,这个 List 就永久存在了——而你本意是它只存活 30 秒。
真正安全的做法是把「入队」和「设过期」封装进一个 Lua 脚本,靠 Redis 单线程执行的特性确保原子性。
- 脚本必须在首次写入时才设置 TTL;如果 List 已存在(比如已有元素),
EXPIRE会重置过期时间,这通常不是你想要的 - 不能对空 List 执行
EXPIRE,否则会返回 0(失败),但脚本不会报错,容易误判 - 推荐用
EXPIREAT配合redis.call("TIME")计算绝对时间戳,避免时钟漂移问题
如何写一个带 TTL 的安全 LPUSH 脚本?
下面这个脚本实现:向 key 对应的 List 右端推入元素,仅当该 key 不存在时设置 TTL(单位秒);若已存在,则只推入,不改 TTL。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local key = KEYS[1]
local value = ARGV[1]
local ttl_sec = tonumber(ARGV[2])
<p>-- 先推入
redis.call("RPUSH", key, value)</p><p>-- 仅当 key 当前无 TTL(即 type == "list" 且未过期)时才设过期
local ttl_now = redis.call("PTTL", key)
if ttl_now == -1 or ttl_now == -2 then
local expire_at = tonumber(redis.call("TIME")[1]) + ttl_sec
redis.call("PEXPIREAT", key, expire_at * 1000)
end</p>
-
PTTL返回-1表示 key 存在但没 TTL,-2表示 key 不存在——两者都该设 TTL - 用
PEXPIREAT(毫秒级)而非EXPIREAT(秒级),精度更高,也和TIME返回值对齐 - 注意
redis.call("TIME")返回的是数组{seconds, microseconds},只取[1]
用 EVAL 调用时要注意哪些参数陷阱?
调用上面脚本时,KEYS 和 ARGV 的顺序、类型、数量必须严格匹配,否则报错 ERR Error running script 或静默失败。
- KEYS 只能传 1 个 key 名(如
"myqueue"),不能传表达式或拼接结果 - ARGV 必须是字符串,数字要转成字符串传入:
EVAL "...” 1 myqueue "hello" "60",不能写60(无引号) - 如果用
redis-cli --eval,注意文件路径后要跟:分隔符,再写 KEY 和 ARGV,例如:redis-cli --eval queue_push.lua myqueue : "hello" "30" - 在 Python 的 redis-py 中,用
eval(script, 1, key, value, ttl_str),第二个参数是 KEYS 数量,不是总数
为什么不能用 LPOP + TTL 判断队列是否“有效”?
有人想靠 TTL key 返回值判断队列是否还“活跃”,但这是错的:只要队列非空,TTL 就始终返回剩余秒数;而你真正关心的是“这个队列是不是还在业务有效期里”。更糟的是,LPOP 本身不检查 TTL,哪怕队列已过期,只要没被 Redis 清理(惰性删除),LPOP 仍能取出旧数据。
- Redis 不会在过期时立即删 key,而是靠访问时惰性删除 + 后台周期性抽查;所以过期的 List 可能“残留”很久
- 安全做法是:消费者每次
LPOP前,先PTTL检查,若返回-2(key 不存在)或-1(无 TTL)且你期望它有 TTL,就说明异常;但更推荐让生产者端严格控制生命周期 - 真正健壮的方案是加一层元信息:比如用 Hash 存
{"created_at": "...", "ttl_sec": "30"},和 List 绑定操作
最易被忽略的一点:Lua 脚本里的 TIME 是 Redis 服务器本地时间,如果客户端和服务端时钟不同步超过 TTL,PEXPIREAT 就会失效——务必确保 NTP 同步。










