不能直接用lpop+rpush维护固定长度,因二者非原子操作,中间存在竞态窗口,易致超长;唯一可靠方式是rpush后立即ltrim key -n -1(保留最新n条)或用lua脚本原子封装。

为什么不能直接用 LPOP + RPUSH 维护固定长度?
因为并发写入时,LPOP 和 RPUSH 是两个独立命令,中间可能插入其他客户端的操作,导致列表长度突破上限。比如:A刚 LPOP 一个元素,B紧接着 RPUSH 一个新元素,但A还没来得及 RPUSH,此时列表已多出一项——这不是竞态,是逻辑漏洞。
必须用原子操作保证「删旧+添新」一步到位:
- 正确姿势是
LTRIM配合RPUSH:先RPUSH新元素,再用LTRIM截断到前 N 项(即保留最新 N 条) - 或者用 Lua 脚本封装
LPOP+RPUSH+LLEN判断,但没必要——LTRIM更轻量、更可靠 -
LPUSH+LTRIM也行,但语义上「最新动态」习惯从右端追加,读取时LRANGE list 0 -1自然按时间正序(最老→最新),而RPUSH+LTRIM配合LRANGE list -N -1才能拿到最新 N 条(倒序截取)
RPUSH 后紧跟 LTRIM 的具体写法和边界注意点
假设要维持最多 100 条最新动态,key 为 feed:latest:
RPUSH feed:latest "event:12345" LTRIM feed:latest -100 -1
关键在 LTRIM 的索引:负数索引从右往左数,-100 表示倒数第 100 个元素(即最新第 100 条),-1 是最后一个(最新一条)。这样截取的结果就是「最新的 100 条」。
容易踩的坑:
- 误写成
LTRIM feed:latest 0 99:这是取最老的 100 条,完全反了 - 忽略空列表情况:
LTRIM对空 key 安全,但若初始无数据,RPUSH后长度为 1,LTRBIM ... -100 -1依然生效(只保留那 1 个) - 不要用
llen+ 条件判断再删:增加一次往返,且无法避免并发间隙
如何保证「秒级滚动」不丢数据、不重复?
秒级滚动本质是高频写入下的稳定性问题,核心不是 Redis 命令,而是客户端调用节奏:
- 写入必须走 pipeline:把
RPUSH和LTRIM打包发,减少网络往返,避免中间被其他命令插入 - 禁止用多个独立 TCP 请求交替写入;哪怕用异步 client,也要确保这两个命令原子提交
- 如果动态来源是 Kafka 或 webhook,需在应用层做简单合并:同一秒内多条事件,可先本地 buffer,到整秒边界统一
RPUSH多个值(RPUSH key v1 v2 v3),再LTRIM——否则单条写入压力大,且秒内顺序难保 - 不依赖 Redis 过期时间控制滚动:List 本身不支持 per-item TTL,别给 key 设
EXPIRE,否则整个列表会消失
读取最新 N 条时,LRANGE 的索引怎么算才不翻车?
写入用的是 RPUSH + LTRIM ... -N -1,那么读最新 N 条,应该用:
LRANGE feed:latest -100 -1
而不是 0 -1 或 -100 0。原因:
-
LRANGE的 start/end 是闭区间,且负数索引始终从右端计数 -
-100是倒数第 100 个(最新第 100 条),-1是最后一条(最新第 1 条),所以-100 -1拿到的就是严格最新的 100 条,按时间升序排列(最老→最新) - 如果想降序(最新→最老),得用
LRANGE feed:latest -1 -100,但注意:Redis 要求 start ≤ end,所以这会返回空;正确做法是先LRANGE再在客户端反转,或改用REV类库辅助 - 当列表实际不足 N 条时(如刚启动),
LRANGE ... -100 -1会自动从开头截取,安全无错
真正麻烦的是分页场景:用户下拉加载更多,不能靠 offset 算——List 没有游标,LRANGE 用偏移量查历史数据效率低且不准。真要分页,得换 Sorted Set 存时间戳,List 只做实时滚动窗口。











