zset 是社交信息流缓存的合理选择,因其有序性、成员唯一性及zrange/zrevrange命令天然支持按时间/热度排序、分页、去重与范围查询。

为什么 ZSET 是社交信息流缓存的合理选择
因为社交 Feed 天然需要按时间(或热度)排序,且需支持分页、去重、范围查询——ZSET 的有序性 + 成员唯一性 + ZRANGE/ZREVRANGE 命令刚好覆盖这些需求。用 String 或 LIST 存原始数据再靠应用层排序,会把压力推到业务代码,也难以做高效分页(比如“拉取第 1000 条之后的 20 条”)。
关键点:score 必须用可比较的时间戳(如毫秒级 System.currentTimeMillis()),不能用秒级时间戳——否则同一秒内多条内容会因 score 冲突导致覆盖(ZADD 默认 XX 不生效时会新增,但若没显式控制,成员可能被意外替换)。
如何用 Lua 脚本安全地清理过期 Feed 缓存
Redis 的 LRU/LFU 驱逐策略不可控:它不区分冷热数据,可能刚刷进缓存的热门 Feed 被误删;而社交 Feed 通常有明确生命周期(如“只保留最近 7 天”),更适合主动清理。
用 Lua 脚本实现原子化清理,避免 ZCARD → ZREMRANGEBYSCORE 之间的竞争条件:
local cutoff = tonumber(ARGV[1])
local key = KEYS[1]
return redis.call('ZREMRANGEBYSCORE', key, '-inf', cutoff)
调用示例:redis-cli --eval zrem_old.lua feed:user:123 , 1698765432000(注意毫秒时间戳)。脚本里用 cutoff 作上界,保留所有 score > cutoff 的项。
- 务必在脚本中用
tonumber()转换参数,否则ZREMRANGEBYSCORE会报错ERR value is not an integer or out of range - 不要在脚本里写
ZCARD后再决定删多少——这破坏原子性,且高并发下可能误删更多 - 清理频率建议设为每小时一次,而非实时触发;太频繁的
ZREMRANGEBYSCORE在大 ZSET 上可能阻塞主线程
缓存写入时如何避免重复和延迟不一致
用户发帖后,既要推送到关注者 Feed(写扩散),又要保证自己主页 Feed 有最新内容。常见错误是只更新 feed:user:123,却忘了同步 feed:timeline:123(个人主页)。
推荐做法:用 pipeline 批量写入多个 key,并显式设置 TTL(哪怕只是逻辑过期):
PIPELINE: MULTI ZADD feed:user:456 1698765432000 post:abc ZADD feed:timeline:123 1698765432000 post:abc EXPIRE feed:user:456 604800 EXPIRE feed:timeline:123 604800 EXEC
注意:EXPIRE 对 ZSET 整体生效,不影响内部 score;TTL 设为 7 天(604800 秒)是兜底,主清理逻辑仍靠 Lua 脚本——因为 EXPIRE 不保证准时触发,且无法按 score 精确裁剪。
- 不要依赖
SET+EXPIRE分两步,中间失败会导致无 TTL - 如果使用 Redis 7.0+,可考虑
ZADD ... XX避免覆盖已有成员,但需确认业务是否允许丢弃同秒重复提交 - 关注关系变更(如取关)时,要异步反向清理对方 Feed 中的历史内容,否则出现“已取关却还能看到对方新帖”
当 ZSET 成员数超百万时,哪些操作必须警惕
ZREVRANGE feed:user:123 0 99 看似安全,但若该 key 有 200 万成员,Redis 仍需定位到倒数第 100 个位置,时间复杂度是 O(log N + M),M 是返回元素数——实际延迟可能达几十毫秒。
更危险的是 ZCOUNT 和 ZCARD:它们在底层需遍历跳表头节点,N 超过 100 万时响应可能抖动。生产环境应监控 zset_length 指标,对超大的 Feed 分片(如按月拆成 feed:user:123:202310)。
- 禁止在业务主链路中执行
ZREMRANGEBYRANK(如删前 1000 条),它会强制遍历跳表,CPU 开销极大 - 分页游标建议用 score + id 组合(如
1698765432000:post:abc),而非纯 offset,规避深度分页问题 - 定期用
MEMORY USAGE feed:user:123查看内存占用,ZSET 的跳表结构比 LIST 更耗内存,单个 key 超 500MB 就该预警
真正麻烦的不是怎么加缓存,而是怎么让缓存不成为瓶颈——ZSET 的有序性带来便利,也绑定了它的扩展边界。分片、冷热分离、脚本原子性,这些都不是可选项,是规模上来了之后绕不开的实操细节。










