Redis 数据结构实战:计数器、排行榜、去重与会话(五)
系列导航:
- 上一篇:Redis 缓存设计:Cache Aside、穿透、击穿与雪崩(四)
- 当前篇:Redis 数据结构实战:计数器、排行榜、去重与会话(五)(本文)
- 下一篇:Redis 消息能力:Pub/Sub、Stream 与队列边界(六)
很多人学 Redis,会先把数据结构一股脑背下来:String、Hash、Set、Sorted Set。但到了业务里,真正的问题通常不是“我会不会这个命令”,而是这个场景为什么更适合这个结构,什么时候继续用下去会开始吃亏。
这一篇只围绕四类最常见的业务场景展开:
- 计数器
- 排行榜
- 去重
- 会话
不展开缓存模式、消息流、分布式锁和复杂搜索,只把这四类事情各自的结构边界讲清楚。
计数器:先问是不是“实时加减”,再问是不是“复杂统计”
计数器是 Redis 最容易做对、也最容易在后面被误用的场景之一。阅读数、点赞数、剩余额度、接口配额、重试次数,这些都很自然地落在 String 上。
最常见的原因很简单:INCR / DECR 的语义足够直接。
INCR article:1001:views
INCRBY user:88:quota:daily 5
DECR coupon:stock:20260808
为什么 String 适合计数器
对于计数器,业务最常见的动作是:
- 当前值加一或减一
- 读取当前值
- 给这个值配一个时间窗口
这些动作都不需要复杂结构。String 的好处在于:
- 命令心智成本低
- 原子递增递减天然可用
- 配合 TTL 很容易做日维度、小时维度窗口
例如“今天接口调用次数”这种场景,通常可以用键名分桶:
api:quota:user:88:2026-08-08
这样做的重点不是 Redis 很神奇,而是你把“统计周期”直接编码进了键模型。
什么时候它开始不好用
计数器一旦发展成下面这些需求,就要开始警惕:
- 想按很多维度做组合统计
- 想同时查明细、趋势和排行
- 想把 Redis 里的实时值直接当作长期报表来源
这时 Redis 仍然可以作为实时层,但不该继续充当全部分析系统。更准确的理解应该是:
- Redis 擅长低延迟计数
- 数据仓库、OLAP 或数据库擅长复杂聚合
如果需求已经从“实时加减”变成“多维分析”,继续往 INCR 上堆功能,只会让键名越来越复杂,最终却仍然难以查询。
排行榜:Sorted Set 最自然,但别把它当万能排序引擎
排行榜是 Sorted Set 的经典场景。因为它刚好同时满足两件事:
- 成员唯一
- 按 score 排序
最基础的建模方式通常是:
ZADD rank:game:daily 98 alice 105 bob 88 carol
ZREVRANGE rank:game:daily 0 9 WITHSCORES
ZINCRBY rank:game:daily 10 alice
为什么 Sorted Set 适合排行榜
排行榜的本质不是“存一份有序列表”,而是不断回答这些问题:
- 当前 Top N 是谁
- 某个成员排第几
- 某个成员的分数有没有变化
Sorted Set 对这类问题非常贴合,因为它原生就把“成员”和“分值”绑在一起。
最常见的 member 和 score 分配方式是:
- member:用户 ID、文章 ID、商品 ID
- score:积分、热度、时间戳、加权分
只要排序维度明确,Sorted Set 往往比把数据堆在数据库里临时排序更直接。
设计排行榜时要先定三件事
很多排行榜后面会出问题,不是因为结构选错,而是因为边界没提前定:
- 是否只保留 Top N,还是要长期保存全部成员
- 排行榜是否按天、周、月分桶
- 分数是否允许反复增减,还是只追加
如果这些问题不先定,最容易出现两个后果:
- 一个排行榜无限增长,最后变成大 Key
- 业务开始要求“按地区、按分类、按时间再筛一层”,结构马上变重
什么时候不该继续靠 Sorted Set
只要你的需求已经变成下面这种复杂排序:
- 先按多字段过滤
- 再按复杂权重排序
- 再做分页、聚合和搜索
那就不是简单排行榜了,而更像数据库或搜索系统的工作。Sorted Set 擅长的是单一排序维度下的快速读写,不是完整的查询引擎。
去重:先分清“要成员集合”还是“只要人数”
去重也是很容易被说模糊的话题。真正要先回答的是:你到底需要知道哪些元素出现过,还是只需要知道大概有多少个不重复元素。
需要精确成员集合时,用 Set
例如:
- 订单号是否处理过
- 用户是否给某篇文章点过赞
- 某个活动今天有哪些用户参与过
这些场景都需要明确判断“这个元素在不在集合里”。此时 Set 非常自然:
SADD article:1001:likes user:88
SISMEMBER article:1001:likes user:88
SCARD article:1001:likes
Set 的优势不是只有“去重”两个字,而是:
- 元素天然唯一
- 存在性判断简单
- 后续还能做交集、并集、差集
如果你后面还要回答“两个标签群体的共同用户是谁”,Set 会比自己维护字符串列表轻松得多。
只关心近似人数时,思路就该变
有些业务并不需要知道每个成员是谁,只想知道大概有多少独立访问者、多少独立设备、多少去重用户数。此时如果仍然把所有成员塞到 Set 里,成本会不断上升。
这种场景更重要的是认知边界:
- 需要精确成员,就用精确集合结构
- 只要近似数量,就应该考虑更偏概率统计的方案
这篇不展开概率结构本身,但至少要记住:不要为了一个“人数”结果,长期保留一整套本来不需要的精确成员明细。
Set 最容易出现的风险
Set 好用,但也很容易无边界增长。尤其在这些场景里:
- 长期保留点赞用户集合
- 活动参与名单没有过期或归档
- 幂等去重键永久堆积
如果你没有为键模型设计生命周期,那么去重最终就会演变成内存膨胀问题。
会话:关注的是“状态是否共享”,不是只会存登录态
很多系统第一次把 Redis 接入业务,不是为了排行榜,而是为了会话。因为只要应用从单实例走向多实例、容器化或网关转发,单机内存会话很快就不够用了。
会话为什么适合放 Redis
会话最核心的需求通常是:
- 多个应用实例都能读到同一份状态
- 会话有自然过期时间
- 读取频繁、写入相对可控
Redis 刚好很适合这三点:
- 共享访问快
- TTL 模型天然适合会话过期
- 键值读写简单,客户端生态成熟
一个基础键模型通常像这样:
session:3f7c6d2e...
值可以是序列化对象,也可以是字段化结构。
String 和 Hash 怎么选
会话数据并不一定只有一种存法。
用 String
适合:
- 整体读整体写
- 会话字段不多,更新频率不高
- 客户端框架本身就按序列化对象处理
优点是简单,缺点是局部字段更新时要整对象重写。
用 Hash
适合:
- 会话字段较多
- 某些字段会被单独更新,例如最近活跃时间、CSRF token、设备信息
优点是字段级更新更直接,缺点是如果每次仍然 HGETALL,收益就会被抵消。
会话最容易忽视的不是结构,而是生命周期
会话场景真正容易出问题的地方,通常不是命令不会写,而是这些边界没定清楚:
- 访问一次是否要续期
- 主动退出时是否立即删除
- 单用户多端会话是否需要拆分
- 会话里到底放最小必要状态,还是把整份用户资料都塞进去
如果会话既当登录态,又当用户资料缓存,又当授权上下文总线,最终很容易变成一个越来越大的对象。
更稳妥的原则是:
- 会话只存“请求链路立即需要”的状态
- 长期资料仍然回到数据库或其他缓存模型
- 生命周期必须清晰,不要让会话键无限悬挂
四类场景放在一起看,真正要比的是“操作语义”
为什么 Redis 在这些场景里好用?不是因为它“比数据库快”这么简单,而是因为这些场景都能找到结构上天然匹配的操作语义:
- 计数器关心加减和窗口,
String正合适 - 排行榜关心分数排序,
Sorted Set正合适 - 去重关心成员唯一,
Set正合适 - 会话关心共享状态和过期,
String/Hash正合适
只要语义匹配,设计就会顺;语义一旦开始偏,比如拿 Sorted Set 做复杂检索、拿 Set 长期存海量明细、拿会话承载过多业务对象,Redis 的优势就会很快变成维护负担。
本篇解决了什么:
- 说明了计数器为什么通常优先落在
String,以及它何时会遇到复杂统计边界 - 解释了排行榜为什么天然适合
Sorted Set,但不该继续膨胀成万能排序系统 - 区分了去重场景里“要精确成员集合”和“只要人数结果”两类不同需求
- 梳理了会话场景中
String/Hash的选择依据,以及生命周期设计的重要性