Redis 02 · 数据结构:String 到 Stream 怎么选

上一篇我们把 Redis 定位成"数据结构服务器":库存、用户资料、领券记录、销量排行和订单事件,虽然都能放进 Redis,却不应该塞进同一种结构。数据结构一旦选错,轻则代码绕、查询慢,重则一次全量读取或集合运算就堵住主线程。

这也是面试里的高频追问:"Redis 有哪些数据类型,各适合什么场景?"只背"String 做缓存、Hash 存对象、List 做队列"远远不够。面试官真正想听的是:为什么这个访问模式适合这种结构,换一种会付出什么代价,以及到了生产环境有哪些边界。尤其是"List 能不能做消息队列""Hash 和 JSON 字符串怎么选",答案都不是一句口诀。

这篇按"先看访问模式,再选数据结构"这条线索展开:先建立统一的选型方法,再依次拆解 String、Hash、List、Set、Sorted Set 和 Stream,最后补上 Bitmap、HyperLogLog、Geo,并用一张选型表把它们放回电商业务。

目录

  1. 选数据结构不是背命令,而是先看访问模式
  2. String:缓存、计数器与简单状态
  3. Hash:对象字段与局部更新
  4. List:有序序列与简单队列
  5. Set:去重、成员判断与集合运算
  6. [Sorted Set:排行榜、优先级与范围查询](#Sorted Set:排行榜、优先级与范围查询)
  7. Stream:可追踪、可确认的消息流
  8. [Bitmap、HyperLogLog、Geo 与最终选型表](#Bitmap、HyperLogLog、Geo 与最终选型表)

一、选数据结构不是背命令,而是先看访问模式

业务数据长什么样,只决定了一半答案;另一半取决于它会被怎样访问。同一份商品信息,可以是一段整体读写的 JSON,也可以是一组需要单独修改的字段。两者看起来都在"存对象",访问模式却完全不同。

选择前先回答五个问题:

  1. 读写粒度是什么? 每次整体读写,还是只读、只改某几个字段?
  2. 是否要求唯一? 是允许重复的序列,还是需要天然去重的集合?
  3. 是否要求顺序? 按插入顺序、按业务分数排序,还是根本不关心顺序?
  4. 主要查询是什么? 按 Key 取值、判断成员、求交集、查 Top N,还是按范围读取?
  5. 消费后要不要确认和恢复? 只是取走一个任务,还是必须知道消息由谁处理、哪些尚未确认?

可以先用一张极简地图建立直觉:

访问模式 优先考虑 典型业务
一个 Key 对应一个值,整体读写 String 商品详情 JSON、验证码、库存计数
一个对象包含多个可独立访问的字段 Hash 商品属性、用户会话
保留插入顺序,从两端操作 List 最新记录、简单任务队列
成员唯一,做存在判断或集合关系 Set 已领券用户、共同关注
成员唯一,还要按分数排序 Sorted Set 销量榜、优先级任务
事件持续追加,需要消费组、确认与恢复 Stream 订单事件流

这里有两个容易忽略的前提。

第一,Redis 的数据类型是 Key 对应的 Value 类型 ,Key 本身仍是字符串。第二,Redis 命令的快慢不能只看复杂度字母,还要乘上真实数据规模。HGETALLSMEMBERS、大范围 ZRANGE 都可能随着返回元素增多而变重;一次向客户端回传几十万个成员,即使查找过程不复杂,网络和序列化成本也不会消失。

选型的核心不是"Redis 能不能做",而是让服务端原生命令直接表达最频繁的业务动作,并让单次操作的成本可控。 有了这把尺子,再看各类型就不会学成命令清单。

二、String:缓存、计数器与简单状态

String 是 Redis 最基础、覆盖面也最广的类型。它保存的是二进制安全的字节序列,所以内容既可以是普通文本、数字和 JSON,也可以是序列化后的二进制数据。名字叫 String,不代表它只能放"字符串"。

2.1 整体读写:商品详情缓存

如果商品详情总是作为一个整体读取和刷新,把它序列化成 JSON 放进 String 最直接:

redis 复制代码
SET product:detail:1001 '{"name":"机械键盘","price":39900,"stock":500}' EX 600
GET product:detail:1001

这种方案的优点是应用侧模型清晰,一次 GET 就拿到完整对象;序列化格式也不受 Redis 字段模型约束。代价是只改价格时仍要读取或重新生成整个值,而且并发"读出---修改---写回"可能互相覆盖。

所以 String 适合整体读、整体写的对象缓存;如果频繁局部更新字段,Hash 通常更自然。 不过别只为了"对象"两个字就机械换成 Hash。如果每次接口都要完整返回商品,Hash 最终还是要读取全部字段,未必比一段紧凑的序列化结果更划算。

2.2 原子增减:计数器与库存

Redis 能把保存为整数形式的 String 直接做原子增减:

redis 复制代码
SET product:stock:1001 500
INCRBY product:stock:1001 -1

INCR product:view:1001

INCRINCRBY 的价值不只是少写一行代码,而是把"读取旧值、计算新值、写回"收进一条原子命令,多个客户端并发执行不会丢失更新。

但原子减一不等于完整的秒杀方案。库存为 0 时,INCRBY ... -1 仍然可以减成负数;"先 GET 判断,再 DECR"又不是一个原子整体。要保证库存不能小于 0,需要 Lua 或其他原子组合,同时还要处理订单落库、回滚和幂等。《Redis 07 · 原子操作:事务、Pipeline、Lua 与 Functions》会继续拆这个问题。

2.3 条件写入:锁、幂等与短期状态的积木

SET 可以把"仅不存在时写入"和"过期时间"组合在一条命令里:

redis 复制代码
SET order:idempotent:req-8f31 1 NX EX 60
SET login:code:13800000000 482913 EX 300

第一条适合做短期幂等标记,第二条适合验证码一类短期状态。这里的关键是 写入与设置 TTL 在同一条命令中完成,避免进程恰好在两条命令之间崩溃,留下永不过期的 Key。

SET NX EX 也是实现分布式锁的基础积木,但"抢到一个带超时的 Key"不等于锁方案已经完整。锁的唯一标识、安全释放、业务超时、续期和故障切换都有额外边界,第 13 篇再展开。

生产中还要留意三件事:

  • 不要把超大 JSON、图片或文件直接塞进 String;单个值即使允许很大,也会放大网络、复制、持久化和删除成本;
  • 计数器要考虑整数范围、Key 生命周期和热点写入;
  • 缓存值要有版本兼容和反序列化失败的处理策略。

String 解决的是"一个 Key 对应一个整体值"。当业务开始要求只读价格、只改库存、只让会话中的某个字段过期,问题就从整体读写变成了字段访问。

三、Hash:对象字段与局部更新

Hash 可以理解成 Redis Key 下面再挂一张字段表:外层 Key 定位对象,field 定位属性,value 保存属性值。

redis 复制代码
HSET product:1001 name "机械键盘" price 39900 stock 500
HGET product:1001 price
HMGET product:1001 name price stock
HINCRBY product:1001 stock -1

它特别适合两个条件同时成立的场景:对象由多个字段组成,而且调用方经常只访问或修改其中一部分。 例如后台只更新商品价格,库存服务只修改库存,详情接口再读取所需字段,不必每次传输和重写整个对象。

3.1 Hash 和 JSON String 怎么选

这是一道很常见的面试题,不能简单回答"存对象就用 Hash"。

对比维度 JSON String Hash
读写粒度 整体读写自然 字段读写自然
应用模型 一次反序列化为完整对象 需要处理 field/value 映射
局部更新 通常要重写整个值 HSET 可直接改字段
网络传输 完整对象每次传输 可以只取需要的字段
复杂嵌套 JSON 表达更自然 通常需要拆平或再次序列化
并发修改 整体覆盖容易丢更新 不同字段可独立修改

如果商品详情始终整块返回,JSON String 往往更省心;如果购物车里每个商品的数量要频繁独立增减,Hash 更贴合访问方式:

redis 复制代码
HSET cart:user:42 product:1001 2 product:2008 1
HINCRBY cart:user:42 product:1001 1
HGET cart:user:42 product:1001

3.2 Hash 不是免费的"小表"

把 Hash 类比成一张小表有助于理解字段访问,但不要把它当关系型数据库。它没有任意条件查询、二级索引和 JOIN;你仍然要先知道外层 Key,才能访问其中字段。

HGETALL 会返回全部字段,成本会随字段数量和响应大小增长。会无限增加字段的购物车、设备状态或用户属性,不能每次都全量拉取;需要渐进遍历时使用 HSCAN,同时给对象规模设置边界。

另一个常见误区是 TTL。传统上过期时间主要挂在整个 Key 上,给 product:1001 设置 TTL 会让整张 Hash 一起过期,而不是只让 price 过期。新版本 Redis 已提供 Hash 字段级过期能力,但客户端支持、部署版本和迁移兼容必须确认,不能拿新语法默认所有生产环境都可用。

版本提示:Hash 字段级过期命令是 Redis 7.4 引入的能力;如果专栏基线内还要兼容更早的 7.x,设计时仍应按"TTL 作用于整个 Hash Key"处理,或把不同生命周期的数据拆成不同 Key。

Hash 在字段维度组织对象,List 则换了一条轴:它关心的是元素的先后顺序。

四、List:有序序列与简单队列

Redis List 是按插入顺序排列的字符串序列,可以从头尾两端推入和弹出元素。它最自然的操作不是"按下标随机访问",而是"维护两端不断变化的队列"。

redis 复制代码
LPUSH user:42:recent-products 1001
LPUSH user:42:recent-products 2008
LTRIM user:42:recent-products 0 99
LRANGE user:42:recent-products 0 9

这组命令维护用户最近浏览的 100 个商品,并取出最新 10 个。LPUSH 后紧跟 LTRIM 很常见,因为 任何会持续增长的 List 都必须有容量治理;否则"最近记录"迟早变成"所有历史记录"。如果要求两条命令不可被其他操作穿插,还要用事务或脚本组合。

4.1 List 适合从两端操作,不适合任意位置查询

List 可以按下标读取,也可以取范围,但越深入中间位置,访问成本通常越高。把它当数组频繁执行大下标 LINDEX,或者一次 LRANGE 0 -1 拉出全部历史,都会偏离它的优势。

List 更适合:

  • 最新 N 条记录;
  • 简单的先进先出或后进先出序列;
  • 生产者推入、消费者阻塞弹出的轻量任务分发。
redis 复制代码
LPUSH order:queue order-90001
BRPOP order:queue 5

BRPOP 在队列为空时可以阻塞等待,避免消费者不断轮询。若任务处理需要更可靠的交接,可以用 LMOVE 把元素原子转入"处理中"列表,再由业务完成后删除;这比直接弹出后立刻从 Redis 消失多了一层补救空间。

4.2 List 能做队列,但不等于可靠消息系统

直接弹出有一个明显窗口:消费者拿到任务后、业务处理完成前宕机,任务已经离开队列,却没有处理成功。即便增加"处理中"列表,消费者身份、超时任务认领、投递次数和积压观测也要自行设计。

因此可以这样划线:只要求有序交接、失败可容忍或业务能自行补偿时,List 足够简单;要求消费组、显式确认、失败认领和待处理追踪时,优先考虑 Stream 或专业消息队列。

List 解决了顺序,却不保证元素唯一。领券名单、共同关注和标签关系更关心"成员是否存在",这正是 Set 的位置。

五、Set:去重、成员判断与集合运算

Set 是无序且成员唯一的集合。重复添加同一个成员不会产生第二份,因此它天然适合去重和存在性判断。

redis 复制代码
SADD coupon:88:users 42
SADD coupon:88:users 42
SISMEMBER coupon:88:users 42
SCARD coupon:88:users

用户 42 即使重复提交,集合里仍只有一个成员。SISMEMBER 可以直接回答"是否领过券",SCARD 可以统计领取人数。这比把用户 ID 拼成一个 JSON 数组,再由应用读取、遍历、去重自然得多。

5.1 集合运算把关系计算留在服务端

Set 还能求交集、并集和差集:

redis 复制代码
SINTER campaign:a:users campaign:b:users
SDIFF coupon:88:eligible coupon:88:users

第一条找出同时参加两个活动的用户,第二条找出有资格但尚未领券的用户。服务端原生命令让业务语义非常清楚,不必把两个集合都拉到应用内存再计算。

但"命令方便"不等于"规模不限"。集合运算需要读取和比较多个集合,成本随参与集合及结果规模增长;结果本身很大时,返回网络也会成为压力。线上不要对不受控的大集合随意执行 SMEMBERSSINTER,尤其不能把它们放在高频接口里。

生产设计通常会补三条约束:

  1. 给 Set 的成员规模设上限并持续监控;
  2. 只需渐进查看成员时使用 SSCAN,不要一次取完;
  3. 大规模离线关系计算交给更适合的计算系统,Redis 保存最终结果或热点子集。

5.2 无序是语义,不要依赖返回顺序

Set 的输出顺序不构成稳定契约。今天看起来按某种顺序返回,不代表下一次或数据规模变化后仍然如此。需要按时间、销量或权重排序时,不要额外在客户端猜顺序,而应该直接使用 Sorted Set。

六、Sorted Set:排行榜、优先级与范围查询

Sorted Set 和 Set 一样保证成员唯一,但每个成员还关联一个 score,Redis 按分数维护顺序。它解决的不是普通排序,而是 数据持续变化时仍要高效地更新名次、查 Top N 和按分数范围读取。

6.1 排行榜:成员是商品,分数是销量

redis 复制代码
ZINCRBY sales:rank 1 product:1001
ZINCRBY sales:rank 3 product:2008
ZRANGE sales:rank 0 9 REV WITHSCORES
ZREVRANK sales:rank product:1001

每卖出一件商品就增加对应分数,读取榜单时直接取分数最高的前 10 名。与"把所有商品销量读出来再排序"相比,Sorted Set 把维护有序关系变成了数据结构本身的职责。

成员必须唯一。如果同一个商品需要同时进入"日榜"和"周榜",通常建立两个 Key,而不是在一个 Sorted Set 里给成员保存两份分数:

text 复制代码
sales:rank:2026-08-17
sales:rank:2026-W34

分数相同时,成员会按字典序进一步排序。因此业务若要求"同销量时先达到者排名更高",不能指望 Redis 自动理解这个规则,需要把时间等信息编码进排序方案,或者接受业务层二次处理。复合分数要特别谨慎:浮点精度、数值范围和更新规则都必须可证明,不能靠拼一个"大概能排"的数字。

6.2 分数也可以表示时间

当 score 保存执行时间戳,Sorted Set 就能表达"哪些任务已经到期":

redis 复制代码
ZADD order:timeout 1786939200000 order-90001
ZRANGE order:timeout -inf 1786939200000 BYSCORE LIMIT 0 100

消费者查询当前时间之前的订单,拿到需要关闭的超时任务。这是延迟任务的一种常见实现,但查询、领取和删除必须避免多个消费者重复处理,通常需要 Lua 做原子认领;消费者停机后的补偿、持久化和集群故障也要评估。

所以 Sorted Set 可以做规模可控的延迟调度,却不自动等于完整的延迟消息平台。数据结构只解决"按时间排序和范围查询",不会替业务解决可靠投递。

6.3 控制范围,别让 Top N 变成 All

Sorted Set 擅长范围查询,但返回成本仍与结果数量有关。Top 10 很轻,不代表 ZRANGE 0 -1 读取百万成员也轻。排行榜应明确窗口,历史榜单应设置保留周期,超大集合还要考虑拆分。

从 List 到 Sorted Set,我们处理的仍然是"元素集合"。订单事件则多了另一个维度:消息不仅要有顺序,还要知道消费进度和处理结果。

七、Stream:可追踪、可确认的消息流

Stream 是持续追加的消息日志。每条消息有唯一 ID 和多个 field/value,既可以按 ID 范围读取,也可以通过消费者组把消息分发给不同消费者。

redis 复制代码
XADD order:events * orderId 90001 status created
XADD order:events * orderId 90001 status paid
XRANGE order:events - + COUNT 10

* 让 Redis 生成消息 ID。ID 通常体现时间顺序,但业务不应把它当订单号;订单号仍然放在消息字段里。

7.1 消费者组解决了什么

创建消费者组后,多个消费者可以分担同一条 Stream 中的新消息:

redis 复制代码
XGROUP CREATE order:events fulfillment 0 MKSTREAM

XREADGROUP GROUP fulfillment worker-1 COUNT 10 BLOCK 5000 STREAMS order:events >

XACK order:events fulfillment <message-id>

消费者通过 XREADGROUP 读取消息后,消息不会因为"被读过"就彻底消失。Redis 会把已投递但尚未确认的消息记录在消费者组的 Pending Entries List(PEL,待处理条目列表) 中;业务处理完成后再用 XACK 确认。

这正是 Stream 和简单 List 队列最关键的区别:

能力 List 队列 Stream 消费者组
顺序保存 支持 支持,消息带 ID
多消费者分工 可以弹出竞争 消费者组原生支持
显式确认 无原生 ACK XACK
未确认追踪 需要自建处理中列表 PEL 原生记录
故障后认领 需要自行设计 可检查并认领超时消息
历史范围读取 不自然 可按 ID 范围读取

worker-1 读取后宕机,运维或其他消费者可以用 XPENDING 检查积压,再用 XAUTOCLAIM 认领空闲超过阈值的消息。这套机制提供了恢复抓手,但不会自动替你完成恢复流程。

7.2 有 ACK 也不等于恰好一次

考虑这样一个时序:消费者已经把订单写入数据库,但还没来得及 XACK 就宕机。消息稍后被重新认领,业务会再处理一次。因此 Stream 常见的是 至少一次投递语义,消费者仍要根据订单号或事件 ID 做幂等。

反过来,如果业务还没真正完成就提前 ACK,随后宕机,消息不会再出现在 PEL 中,可能造成丢处理。正确顺序通常是:读取消息,幂等执行业务,业务成功后 ACK;失败消息的重试次数和最终去向也要明确。

7.3 Stream 也必须限制长度

追加日志如果永不裁剪,最终一定吃满内存。生产中应根据保留时间、消息速率和故障恢复窗口设计裁剪策略,例如追加时设置近似最大长度:

redis 复制代码
XADD order:events MAXLEN ~ 100000 * orderId 90002 status created

~ 表示近似裁剪,通常比精确维护长度更高效,但实际条目数可能略高于目标。裁剪前还要确认消费者是否允许落后到保留窗口之外;如果消息承担审计、跨地域传输、超长积压或严格可靠性要求,应评估 Kafka、RabbitMQ 等专业消息系统,而不是仅凭 Redis 已经在线就顺手复用。

Stream 提供的是消息 ID、消费组、PEL、ACK 和故障认领这些可靠消费积木,不是"零配置的恰好一次消息队列"。 这是面试回答里最值得说清的一层。

八、Bitmap、HyperLogLog、Geo 与最终选型表

前六种类型覆盖了大多数业务,但 Redis 还提供几类面向特定问题的结构。它们不是"高级版通用容器",而是用更强的约束换取空间或查询优势。

8.1 Bitmap:把二值状态压进每一个 bit

Bitmap 适合用户在某一天是否签到、某个功能是否开启这类只有 0/1 的状态。它本质上是在 String 上按位操作,把用户编号映射为偏移量:

redis 复制代码
SETBIT attendance:2026-08-17 42 1
GETBIT attendance:2026-08-17 42
BITCOUNT attendance:2026-08-17

如果用户 ID 稠密且能稳定映射为较小的非负整数,Bitmap 非常节省空间;如果 ID 极度稀疏,例如直接拿一个很大的随机 ID 当偏移量,中间空洞也要占据地址空间,优势就没了。它只能表达 bit 状态,不能顺便保存签到时间、渠道等复杂属性。

多个日期的 Bitmap 还能通过位运算计算连续签到或活跃交集,但同样要评估位图长度和运算频率。Bitmap 适合稠密编号的二值状态,不是 Set 的无条件替代品。

8.2 HyperLogLog:用近似值换极小空间

如果只关心商品详情页"有多少独立访客",不关心访客具体是谁,可以把用户 ID 加入 HyperLogLog:

redis 复制代码
PFADD product:1001:uv user:42 user:73
PFCOUNT product:1001:uv

它返回的是基数估算值,不保证绝对精确。优势是内存占用不会随唯一用户数线性膨胀,适合 UV、独立设备数等海量去重计数;代价是不能列出成员,也不能准确判断某个用户是否已经出现。

因此选择很清楚:需要精确人数、成员判断或拿回名单,用 Set;只要大规模去重后的近似数量,用 HyperLogLog。误差是否可接受是业务条件,不是技术人员替业务默认做的决定。

8.3 Geo:围绕经纬度做附近查询

Geo 用来保存经纬度并做距离、半径或边界范围查询,例如查找用户附近的门店:

redis 复制代码
GEOADD store:locations 121.4737 31.2304 store:shanghai-001
GEOSEARCH store:locations FROMLONLAT 121.4800 31.2200 BYRADIUS 5 km WITHDIST ASC COUNT 20

Geo 底层建立在 Sorted Set 之上,所以同一个 Key 也遵循有序集合的成员语义。它适合"附近有哪些点、距离多远"这类查询,不负责复杂路线规划、道路通行时间和多条件地理分析;后者通常需要专业地图服务或空间数据库。

8.4 一张表完成最终选型

把整篇压缩成一张生产选型表:

业务需求 推荐结构 关键理由 主要边界
商品详情整体缓存 String 一次读写完整序列化对象 局部更新要重写,控制值大小
秒杀库存、浏览量 String 原子增减 条件判断需脚本,热点 Key 要治理
商品字段、购物车数量 Hash 字段独立读写和增减 防止字段无限增长,慎用 HGETALL
最近浏览、简单任务队列 List 保序且擅长两端操作 随机访问弱,可靠消费能力有限
已领券用户、关系去重 Set 唯一成员和成员判断 大集合全量读取、集合运算可能很重
销量榜、到期任务 Sorted Set 按 score 排序和范围查询 控制返回范围,可靠认领需额外设计
订单事件流 Stream 消费组、PEL、ACK、故障认领 消费者仍需幂等,必须治理积压和长度
每日签到状态 Bitmap 稠密编号下按 bit 压缩 稀疏大偏移浪费,只能表达二值状态
商品 UV HyperLogLog 小空间估算海量基数 有误差,不能返回具体成员
附近门店 Geo 经纬度距离和范围查询 不适合路线规划和复杂 GIS

最后再校准一个常见误区:数据结构不是选定后永远不变。业务早期的简单任务队列可能用 List 就够了;后来加入多消费组、失败认领和积压追踪,就该迁到 Stream。商品缓存原本整块刷新适合 String;后来价格和库存由不同服务高频更新,也可能拆成 Hash 或多个职责更单一的 Key。

**好的选型不是追求"最强的数据结构",而是让当前最重要的访问模式最直接,同时给规模增长和语义升级留出迁移边界。


这一篇没有把 Redis 数据类型写成命令手册,而是用访问模式把它们串了起来:整体值交给 String,字段访问交给 Hash,两端有序序列交给 List,唯一成员关系交给 Set,带分数的顺序交给 Sorted Set,需要追踪与确认的事件交给 Stream;Bitmap、HyperLogLog 和 Geo 则分别用更强约束解决二值状态、近似基数和地理位置问题。

带走四句话:① 先问读写粒度、唯一性、顺序、查询方式和消费语义,再选 Redis 数据结构;② String 与 Hash 的分界不在"是不是对象",而在整体读写还是字段读写;③ List 能做简单队列,但需要 ACK、待处理追踪和故障认领时,Stream 更贴合问题;④ 任何 O(1) 或范围命令都不能脱离数据规模、返回数量和网络成本谈安全。

下一篇,我们继续往下一层走------《Redis 03 · 底层原理:RedisObject 与核心编码》。同样是 Hash、List 或 Sorted Set,为什么数据少和数据多时内部实现可能不同?SDS、dict、listpack、quicklist、intset 和 skiplist 又怎样在内存、查找与更新成本之间做权衡?

相关推荐
devpotato2 小时前
缓存与数据库更新顺序不一致问题
java·数据库·redis
NeverFear7927 小时前
【黑马点评】短信登录
java·redis·tomcat·intellij-idea
攻城有术8 小时前
专项攻克——重写 Redis 依赖包方法的 6 种实现方式
java·数据库·redis·bootstrap
spencer_tseng9 小时前
kylin redis-6.2.14.tar.gz
linux·redis·kylin·edis-6.2.14
xbgRS9 小时前
Redis基本概念及应用
redis·缓存
草莓熊Lotso9 小时前
【Redis 初阶】特殊数据类型、渐进式遍历与数据库操作生产指南
linux·网络·数据库·redis·tcp/ip·缓存·bootstrap
λqaq79 小时前
Redis 数据库基础:安装、5 大核心数据类型与常用命令
linux·数据库·redis·python·缓存
莫得感情 o10 小时前
Redis 04 · 执行模型:一条命令的生命周期与 I/O 多路复用
redis·缓存
xbgRS10 小时前
Redis使用及数据结构
redis