Redis 之所以强大,很大程度上归功于它丰富且高效的数据结构。Redis 的数据结构可以分为 5 种基础数据结构 和 多种高级/特殊数据结构。
以下是详细的分类、常用命令以及实际业务中的使用场景:
一、 五大基础数据结构
1. String(字符串)
最简单、最基础的数据类型。它是二进制安全的,除了存字符串,还可以存整数、浮点数,甚至存图片/Base64编码的文件(但一般不推荐存大文件)。底层实现是 SDS(简单动态字符串)。
- 常用命令 :
SET,GET,INCR,DECR,EXPIRE,SETNX - 使用场景 :
- 常规缓存:缓存 HTML 页面、JSON 数据、配置信息。
- 计数器 :利用
INCR实现文章阅读量、视频播放量、商品点赞数的原子递增。 - 分布式锁 :利用
SET key value NX EX(不存在才设置,并加上过期时间)实现分布式锁。 - 分布式 Session:将用户的 Session 信息集中存储在 Redis 中,实现多节点 Session 共享。
2. Hash(哈希/字典)
一个键值对(field-value)的集合,非常适合用来存储对象 。相比用 String 存一大段 JSON,Hash 的优势在于可以只修改或获取对象中的某个字段,且更省内存。
- 常用命令 :
HSET,HGET,HMSET,HMGET,HDEL,HINCRBY - 使用场景 :
- 对象存储:存储用户信息(User ID -> {name, age, email})、商品信息。
- 购物车 :
Key为用户 ID,Field为商品 ID,Value为商品数量。方便单独修改某个商品的数量。 - 用户画像/属性:存储用户的各种标签和属性值。
3. List(列表)
有序的字符串列表,支持从两端(头部或尾部)推入和弹出元素。底层通常是 Quicklist(双向链表+压缩列表)。
- 常用命令 :
LPUSH,RPUSH,LPOP,RPOP,LRANGE,BLPOP(阻塞弹出) - 使用场景 :
- 消息队列(轻量级) :利用
LPUSH生产消息,BRPOP(阻塞读取)消费消息,实现简单的异步任务处理。 - 最新文章/列表 :利用
LPUSH插入新数据,LRANGE 0 9获取最新 10 条数据(如最新 10 条评论、朋友圈时间线)。 - 分页查询:对于数据量不大且不需要复杂排序的列表,可以用 List 做简单的分页。
- 消息队列(轻量级) :利用
4. Set(集合)
无序的、唯一的字符串集合。底层是 intset(整数集合)或 hashtable。支持数学上的集合运算(交、并、差)。
- 常用命令 :
SADD,SREM,SISMEMBER,SMEMBERS,SINTER(交集),SUNION(并集),SDIFF(差集),SRANDMEMBER(随机获取) - 使用场景 :
- 标签(Tags) :给文章或商品打标签,如
article:1:tags->{Redis, 数据库, 缓存}。 - 抽奖活动 :利用
SADD报名,SRANDMEMBER或SPOP随机抽取中奖者。 - 共同好友/共同关注 :利用
SINTER计算两个用户的关注列表的交集。 - 去重:记录已经处理过的消息 ID 或已经访问过的用户,防止重复处理。
- 标签(Tags) :给文章或商品打标签,如
5. ZSet / Sorted Set(有序集合)
在 Set 的基础上,为每个元素增加了一个 score(分数/权重),元素根据 score 进行排序。元素唯一,但 score 可以重复。底层是 跳表(SkipList)+ 哈希表。
- 常用命令 :
ZADD,ZREM,ZSCORE,ZRANGE,ZREVRANGE,ZRANGEBYSCORE - 使用场景 :
- 排行榜 :游戏积分排行榜、商品销量榜、微博热搜榜(利用 score 存分数/热度,
ZREVRANGE获取 Top N)。 - 延迟队列 :将 score 设置为任务执行的绝对时间戳 ,后台定时任务轮询
ZRANGEBYSCORE 0 当前时间戳,取出到期的任务执行。 - 带权重的任务队列:VIP 用户的请求 score 更高,优先被消费。
- 滑动窗口限流:利用 score 记录请求的时间戳,统计单位时间内的请求次数。
- 排行榜 :游戏积分排行榜、商品销量榜、微博热搜榜(利用 score 存分数/热度,
二、 高级/特殊数据结构
除了上述 5 种,Redis 还提供了一些针对特定场景优化的特殊数据结构:
6. Bitmap(位图)
本质上还是 String,但提供了针对位(bit) 操作的命令。由于 1 个字节有 8 个 bit,它在统计"是/否"状态时极其节省内存。
- 常用命令 :
SETBIT,GETBIT,BITCOUNT(统计 1 的个数),BITOP(位运算) - 使用场景 :
- 用户签到 :
Key为user:sign:202607,Offset为日期(如 3 号),值为 1。一个月只需 31 bit(不到 4 个字节)。 - 在线状态统计:记录用户 ID 是否在线,1 表示在线,0 表示离线。
- 布隆过滤器 :利用多个哈希函数将数据映射到位图上,用于快速判断一个数据是否存在(存在不一定有,不存在一定没有),常用于缓存穿透防护。
- 用户签到 :
7. HyperLogLog(基数统计)
用于统计海量数据中的不重复元素个数(基数) 。它的最大优势是极省内存 ,无论统计 100 个还是 10 亿个独立数据,都只需要固定 12KB 的内存。缺点是存在 0.81% 的标准误差,且不存储具体元素。
- 常用命令 :
PFADD,PFCOUNT,PFMERGE - 使用场景 :
- 统计 UV(独立访客):统计网站或 APP 每天的独立访问用户数。
- 统计在线人数:不需要知道具体是谁,只需要知道大概有多少人。
8. Geospatial(地理位置)
底层基于 ZSet 实现,用于存储和计算地理位置信息(经度、纬度)。
- 常用命令 :
GEOADD,GEODIST(计算距离),GEOPOS(获取坐标),GEOSEARCH(查找范围内的位置,注:Redis 6.2 后替代了 GEORADIUS) - 使用场景 :
- LBS(基于位置的服务):查找"附近的人"、"附近的餐厅"、"附近的共享单车"。
- 距离计算:计算打车软件中乘客与司机的距离。
9. Stream(消息流)
Redis 5.0 引入,专门为消息队列设计。支持多播、消费者组(Consumer Group)、消息确认(ACK),功能类似 Kafka。
- 常用命令 :
XADD,XREAD,XREADGROUP,XACK - 使用场景 :
- 复杂的异步消息队列:当 List 做的简单队列无法满足"消息持久化、消费者组、消息确认机制、历史消息回溯"等需求时,使用 Stream。
三、 总结与选型指南
在实际开发中,如果你不知道用什么结构,可以参考以下"选型口诀":
- 存单个值/计数器/锁 ➡️ String
- 存对象/部分更新字段 ➡️ Hash
- 存列表/最新消息/简单队列 ➡️ List
- 存唯一集合/交并差集/抽奖 ➡️ Set
- 存排行榜/延迟队列/按权重排序 ➡️ ZSet
- 存签到/状态/0-1开关 ➡️ Bitmap
- 统计 UV/去重数量(允许微小误差) ➡️ HyperLogLog
- 存经纬度/附近的人 ➡️ Geospatial
- 需要完整的消息队列功能 ➡️ Stream
"缓存穿透"、"缓存击穿"和"缓存雪崩"是高并发场景下 Redis 缓存最容易出现的三大经典故障。它们的核心危害都是导致大量请求绕过缓存,直接打到数据库,最终可能压垮数据库。
虽然名字相似,但它们的发生场景、原因和解决方案截然不同。以下是详细的深度解析:
一、 缓存穿透 (Cache Penetration)
1. 什么是缓存穿透?
- 核心特征 :查询的数据在缓存和数据库中都不存在。
- 通俗比喻:你去找一个根本不存在的人,门卫(缓存)说没这人,你只能去屋里(数据库)找,屋里也没有。下次另一个人来找,门卫还是说没这人,又得去屋里找。
- 发生场景 :
- 恶意攻击 :黑客故意用大量不存在的 ID(如
id=-1)发起请求。 - 业务代码 Bug:查询了错误的数据源。
- 恶意攻击 :黑客故意用大量不存在的 ID(如
2. 危害
每次请求都会穿透缓存,直接查询数据库。如果并发量大,数据库会瞬间承受巨大压力。
3. 解决方案
- 方案 A:缓存空值/默认值(最常用)
- 做法 :如果数据库也没查到数据,在 Redis 中缓存一个空值(如
""或null),并设置一个较短的过期时间(如 3~5 分钟)。 - 优点:实现简单,能有效拦截重复的无效请求。
- 缺点:如果攻击者每次使用不同的随机 ID,会导致缓存中充斥大量空值,浪费内存。
- 做法 :如果数据库也没查到数据,在 Redis 中缓存一个空值(如
- 方案 B:布隆过滤器 (Bloom Filter)(终极方案)
- 做法 :在访问缓存之前,先让请求经过布隆过滤器。布隆过滤器能快速判断一个数据是否绝对不存在。如果布隆过滤器说"不存在",则直接拦截,不查缓存和数据库。
- 优点 :极其节省内存,拦截率极高。Redis 4.0+ 提供了
RedisBloom模块。 - 缺点 :存在一定的误判率 (说存在,但实际可能不存在),且不能删除元素。通常结合"缓存空值"一起使用。
二、 缓存击穿 (Cache Breakdown)
1. 什么是缓存击穿?
- 核心特征 :单个热点 Key 在过期的瞬间,恰逢大量并发请求同时访问该 Key。
- 通俗比喻:一堵墙很坚固,但墙上有一个洞(热点 Key 过期),所有的水(高并发请求)都从这个洞里猛烈地喷进来,冲垮了后面的堤坝(数据库)。
- 发生场景 :
- 首页的"爆款商品"、"热搜新闻"等超高并发的数据,其缓存刚好到了过期时间。
- 第一个请求发现缓存失效,去查数据库;此时后续成千上万个请求也发现缓存失效,全部涌向数据库。
2. 危害
瞬间的高并发请求直接打到数据库,导致数据库 CPU 飙升、连接池耗尽甚至宕机。
3. 解决方案
- 方案 A:互斥锁(分布式锁 / 本地锁)
- 做法 :在获取缓存失败时,尝试获取锁(如 Redis 的
SETNX或 Java 的synchronized)。只有获取到锁的线程才去查询数据库并重建缓存;其他没获取到锁的线程,要么休眠重试 ,要么直接返回默认值/错误。 - 优点:保证了数据的一致性,不会有多余的请求打到数据库。
- 缺点:线程需要等待锁,性能有一定下降。
- 做法 :在获取缓存失败时,尝试获取锁(如 Redis 的
- 方案 B:逻辑过期(不设置物理 TTL)
- 做法 :Redis 中的 Key 不设置过期时间 (物理不过期),而是在 Value 中额外存储一个逻辑过期时间。
- 当线程发现逻辑过期时,不阻塞 ,直接返回旧数据给前端。同时,开启一个异步线程去查询数据库并更新缓存。
- 优点:性能极高,没有线程阻塞,用户体验好。
- 缺点 :在异步线程更新完成前,会返回短暂的旧数据(数据一致性要求不高的场景适用)。
三、 缓存雪崩 (Cache Avalanche)
1. 什么是缓存雪崩?
- 核心特征 :大量 Key 在同一时间集体过期 ,或者 Redis 服务直接宕机。
- 通俗比喻:原本挡在前面的大坝(缓存)突然全面崩塌,所有的洪水(请求)瞬间倾泻到下游的村庄(数据库),造成毁灭性打击。
- 发生场景 :
- 开发人员在设置缓存过期时间时,使用了统一的值(如全都是 2 小时),导致 2 小时后缓存集体失效。
- Redis 主节点宕机,且主从切换失败。
2. 危害
整个系统的请求全部打到数据库,导致数据库彻底崩溃,系统全面瘫痪。
3. 解决方案
- 方案 A:针对"大量 Key 同时过期"
- 做法 :在原有的过期时间基础上,加上一个随机值(如 1~5 分钟的随机数)。这样可以将集体过期的时间点打散,避免同一时刻发生雪崩。
- 方案 B:针对"Redis 宕机"(高可用架构)
- 做法 :不要使用单机 Redis。采用 主从复制 + 哨兵模式(Sentinel) 或 Redis Cluster 集群模式,实现自动故障转移。同时开启 RDB 和 AOF 持久化,防止数据丢失。
- 方案 C:兜底方案(限流、降级、多级缓存)
- 服务限流:当发现数据库压力过大时,直接丢弃部分请求,保护核心业务(如使用 Sentinel 限流)。
- 服务降级:返回兜底数据(如"系统繁忙,请稍后再试"或静态默认数据)。
- 多级缓存 :引入本地缓存(如 Caffeine、Guava Cache)。请求先查本地缓存,再查 Redis,最后查数据库。即使 Redis 挂了,本地缓存还能扛住一部分压力。
四、 核心对比与总结
为了在面试或实战中快速区分,请记住以下核心差异:
| 故障类型 | 核心特征 | 数据是否存在 | 发生原因 | 解决核心思路 |
|---|---|---|---|---|
| 缓存穿透 | 查无此人 | 不存在(缓存和DB都没有) | 查询根本不存在的数据 / 恶意攻击 | 拦截无效请求(布隆过滤器、缓存空值) |
| 缓存击穿 | 单点突破 | 存在,但刚好过期 | 单个热点 Key 过期 + 高并发 | 控制并发重建(互斥锁、逻辑过期) |
| 缓存雪崩 | 全面崩溃 | 存在,但集体过期 / Redis挂了 | 大量 Key 同时过期 / Redis 宕机 | 打散过期时间、高可用、限流降级 |
五、 面试实战加分项
如果面试官问:"你们项目中是怎么解决这些问题的?"
不要只背概念,要给出组合拳方案:
"在我们的电商项目中:
- 针对穿透 ,我们在商品详情页使用了布隆过滤器 ,拦截了 99% 的恶意不存在的 ID 请求;对于极少量的漏网之鱼,配合缓存空值(设置 5 分钟过期)来兜底。
- 针对击穿 ,对于首页的'秒杀商品'这种绝对热点,我们采用了逻辑过期 的方案,保证高并发下不阻塞线程;对于普通的商品详情,使用了 Redisson 分布式锁来防止并发重建缓存。
- 针对雪崩 ,我们在设置 TTL 时,统一加上了 1~5 分钟的随机值 ;同时 Redis 采用了 Cluster 集群 保证高可用;并且在网关层配置了 Sentinel 限流,确保即使极端情况下 Redis 出问题,数据库也不会被打垮。"