系列导航:

很多人学 Redis,会先把数据结构一股脑背下来:StringHashSetSorted 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 往往比把数据堆在数据库里临时排序更直接。

设计排行榜时要先定三件事

很多排行榜后面会出问题,不是因为结构选错,而是因为边界没提前定:

  1. 是否只保留 Top N,还是要长期保存全部成员
  2. 排行榜是否按天、周、月分桶
  3. 分数是否允许反复增减,还是只追加

如果这些问题不先定,最容易出现两个后果:

  • 一个排行榜无限增长,最后变成大 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...

值可以是序列化对象,也可以是字段化结构。

StringHash 怎么选

会话数据并不一定只有一种存法。

String

适合:

  • 整体读整体写
  • 会话字段不多,更新频率不高
  • 客户端框架本身就按序列化对象处理

优点是简单,缺点是局部字段更新时要整对象重写。

Hash

适合:

  • 会话字段较多
  • 某些字段会被单独更新,例如最近活跃时间、CSRF token、设备信息

优点是字段级更新更直接,缺点是如果每次仍然 HGETALL,收益就会被抵消。

会话最容易忽视的不是结构,而是生命周期

会话场景真正容易出问题的地方,通常不是命令不会写,而是这些边界没定清楚:

  • 访问一次是否要续期
  • 主动退出时是否立即删除
  • 单用户多端会话是否需要拆分
  • 会话里到底放最小必要状态,还是把整份用户资料都塞进去

如果会话既当登录态,又当用户资料缓存,又当授权上下文总线,最终很容易变成一个越来越大的对象。

更稳妥的原则是:

  • 会话只存“请求链路立即需要”的状态
  • 长期资料仍然回到数据库或其他缓存模型
  • 生命周期必须清晰,不要让会话键无限悬挂

四类场景放在一起看,真正要比的是“操作语义”

为什么 Redis 在这些场景里好用?不是因为它“比数据库快”这么简单,而是因为这些场景都能找到结构上天然匹配的操作语义:

  • 计数器关心加减和窗口,String 正合适
  • 排行榜关心分数排序,Sorted Set 正合适
  • 去重关心成员唯一,Set 正合适
  • 会话关心共享状态和过期,String / Hash 正合适

只要语义匹配,设计就会顺;语义一旦开始偏,比如拿 Sorted Set 做复杂检索、拿 Set 长期存海量明细、拿会话承载过多业务对象,Redis 的优势就会很快变成维护负担。

本篇解决了什么:

  • 说明了计数器为什么通常优先落在 String,以及它何时会遇到复杂统计边界
  • 解释了排行榜为什么天然适合 Sorted Set,但不该继续膨胀成万能排序系统
  • 区分了去重场景里“要精确成员集合”和“只要人数结果”两类不同需求
  • 梳理了会话场景中 String / Hash 的选择依据,以及生命周期设计的重要性

下一篇:Redis 消息能力:Pub/Sub、Stream 与队列边界(六)