不能直接用lpush+ltrim组合做日志队列,因二者非原子操作,并发时易超长;须用lua脚本封装lpush与ltrim并设置expire,lrange读取需防性能陷阱,长期日志应转存外部系统。

为什么不能直接用 LPUSH + LTRIM 组合做日志队列
因为 LPUSH 和 LTRIM 是两个独立命令,在并发写入时无法保证原子性:多个客户端同时 LPUSH 后再 LTRIM,极大概率导致列表长度超过预期。你看到的日志条数飘忽不定、偶发暴涨,基本就是这个原因。
正确做法是用 LPUSh + LTRIM 封装成原子操作——但 Redis 原生命令不支持。所以必须用 Lua 脚本兜底:
eval "redis.call('LPUSH', KEYS[1], ARGV[1]); redis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[2])-1); return 1" 1 log:list 12345 1000
其中 KEYS[1] 是 list key,ARGV[1] 是新日志内容,ARGV[2] 是目标最大长度(比如 1000)。脚本强制把列表裁剪为前 ARGV[2] 个元素,即保留最新 1000 条。
LTRIM 的索引边界容易搞反:0 是最新还是最旧?
Redis List 是栈式结构:LPUSH 往左头插入,所以索引 0 对应的是**最后插入的那条日志**(也就是最新的一条),索引越往后越旧。因此 LTRIM key 0 999 表示“只留从最新开始的前 1000 条”,不是“留最老的 1000 条”。
- 想保留最新 N 条 →
LTRIM key 0 (N-1) - 想保留最老 N 条 →
LTRIM key -N -1(不推荐,日志场景几乎不用) - 误写成
LTRIM key 0 N会多留一条,看似只差 1,但高并发下可能让内存缓慢爬升
生产环境必须设 EXPIRE,否则 list 永远不释放
很多人只关注长度控制,忘了生命周期管理。Redis 中没有自动过期的 List;即使你每秒 LPUSH + LTRIM,只要 key 一直存在,它就一直占内存。更危险的是:如果某次脚本执行失败(如网络中断),LTRIM 没跑,列表可能瞬间膨胀,又没过期时间,OOM 风险陡增。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
安全做法是在第一次写入时就设置过期:
eval "if redis.call('EXISTS', KEYS[1]) == 0 then redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3])) end; redis.call('LPUSH', KEYS[1], ARGV[1]); redis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[2])-1)" 1 log:list "2024-06-10T15:23:42Z ERR timeout" 1000 3600
上面脚本中 ARGV[3] 是 TTL 秒数(如 3600 = 1 小时),仅在 key 不存在时设置一次过期,避免重复调用 EXPIRE 影响性能。
用 LRANGE 读日志时注意游标和性能陷阱
LRANGE log:list 0 -1 看起来方便,但在列表很长(比如 10 万条)时会阻塞主线程、拖慢其他请求。生产中不该全量拉取。
- 前端分页查日志?用
LRANGE log:list {offset} {offset+limit-1},但 offset 超过 10k 后延迟明显上升 - 更稳的做法是配合时间戳字段:每条日志存成
"[ts] msg",用LRANGE拿最近几百条,再由应用层过滤时间范围 - 绝对不要在 Lua 脚本里调用
LRANGE返回大量数据,Redis 会把整个结果塞进脚本上下文,内存翻倍且不可控
真正要长期存档的日志,别卡在 Redis 里——到长度或时间阈值后,用后台任务 LPOP 批量消费并转存到 ES 或对象存储,Redis 只做滑动窗口缓冲。










