Redis
基础
Redis 是一个基于内存的高性能 键值型 NoSQL 数据库,通常用作缓存、中间件、消息队列等。
redis为什么快
纯内存操作 (Memory-Based Storage) :这是最主要的原因。Redis 数据读写操作都发生在内存中,访问速度是纳秒级别,而传统数据库频繁读写磁盘的速度是毫秒级别,两者相差数个数量级。
高效的 I/O 模型 (I/O Multiplexing & Single-Threaded Event Loop) :Redis 使用单线程事件循环配合 I/O 多路复用技术,让单个线程可以同时处理多个网络连接上的 I/O 事件(如读写),避免了多线程模型中的上下文切换和锁竞争问题。虽然是单线程,但结合内存操作的高效性和 I/O 多路复用,使得 Redis 能轻松处理大量并发请求(Redis 线程模型会在后文中详细介绍到)。
优化的内部数据结构 (Optimized Data Structures) :Redis 提供多种数据类型(如 String, List, Hash, Set, Sorted Set 等),其内部实现采用高度优化的编码方式(如 ziplist, quicklist, skiplist, hashtable 等)。Redis 会根据数据大小和类型动态选择最合适的内部编码,以在性能和空间效率之间取得最佳平衡。
简洁高效的通信协议 (Simple Protocol - RESP) :Redis 使用的是自己设计的 RESP (REdis Serialization Protocol) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。
为什么用redis
1、访问速度更快
传统数据库数据保存在磁盘,而 Redis 基于内存,内存的访问速度比磁盘快很多。引入 Redis 之后,我们可以把一些高频访问的数据放到 Redis 中,这样下次就可以直接从内存中读取,速度可以提升几十倍甚至上百倍。
2、高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g),但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
3、功能全面
Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大!
为什么不用本地缓存
虽然本地缓存(如使用 Java 的 Map
或 Guava
缓存)访问速度更快,但在分布式系统中,本地缓存存在一些明显的局限,而 Redis 恰好可以解决这些问题。
首先,本地缓存是进程内存级别的,每个服务节点维护自己的缓存副本,数据无法共享,一致性难以保证。当一个节点更新了缓存,其他节点并不知情,容易出现脏读。而 Redis 作为一个独立的服务,可以作为全局缓存中心统一管理数据,避免这种一致性问题。
其次,本地缓存容量受限于单个服务实例的内存,无法支撑大规模缓存需求;而 Redis 是独立部署的,支持大内存、高并发,可以更灵活地扩展。
再者,Redis 支持丰富的数据结构和高级特性,如过期时间、LRU 淘汰策略、分布式锁、持久化、发布订阅等,这些功能是本地缓存很难实现的。
此外,在服务重启或扩缩容时,本地缓存会被清空,而 Redis 可以长期保存热点数据,避免缓存重新预热带来的性能抖动。
应用
redis能做什么
redis用作数据缓存,这是最常见的用途,比如缓存用户信息、热点商品数据、页面渲染结果,减轻数据库压力,加快响应速度。
另外,Redis 可以用作分布式锁,利用其原子性操作保障多节点环境下对共享资源的互斥访问,常用于防止超卖、重复提交等问题。
其次,Redis 支持消息队列功能,可以通过 list、stream 或发布订阅机制实现简单的异步通信,用于系统解耦、异步处理等场景。
此外,Redis 常用于计数器和限流器,比如统计接口调用次数、用户行为次数,结合过期时间可以快速实现限流、频控等需求。
Redis 还常被用作会话存储(Session 共享),在分布式系统中统一管理用户登录状态,避免单点服务丢失会话信息。
在业务层面,Redis 的有序集合结构适合实现排行榜、点赞数、活跃用户统计等功能,Geo 类型也可以支持地理位置存储与距离计算。
最后,Redis 还可用来实现延迟队列、任务调度、热点数据预加载等需求,充分发挥其高性能和多结构的特点。
redis实现分布式锁
基于 Redis 实现分布式锁的核心思路,是利用 Redis 提供的原子性命令 SET key value NX EX
。这个命令可以在 key 不存在时设置值,并指定过期时间,保证只有一个客户端能成功加锁,从而达到互斥的效果。
实现过程主要包括几个关键点:
- 加锁(互斥):使用
SET key value NX EX
,其中 NX 表示“仅当 key 不存在时设置”,EX 设置过期时间,避免死锁。 - 锁唯一性:value 通常设置为唯一标识(比如 UUID),用于标记是哪一个客户端加的锁,防止误解锁。
- 防止死锁:设置合理的过期时间,即使客户端异常宕机,锁也能自动释放。
- 释放锁(安全解锁):解锁时需要判断 value 是否一致,只有加锁的客户端才能释放对应的锁,避免误删其他线程的锁。这通常需要使用 Lua 脚本来实现 check-and-delete 的原子操作。
除了单实例实现方式,Redis 官方也提供了分布式场景下更稳健的 Redlock 算法,它通过在多个 Redis 节点上同时加锁、获取多数派响应,以提高分布式环境下的容错能力和可用性。
redis做消息队列
Redis 可以用来实现消息队列,并且在一些对可靠性要求不高的轻量场景中,它是一种非常实用的解决方案。
Redis 提供多种数据结构支持队列功能,比如:
- 使用 list 搭配
LPUSH + BRPOP
实现最基本的先进先出队列; - 使用 stream 类型可以支持多消费者组、消费确认、阻塞读取,更接近完整的消息队列能力;
- 也可以用 pub/sub 实现实时广播消息,用于通知类场景。
Redis 实现的消息队列具有响应快、部署简单、集成成本低的优点,非常适合用于异步处理、系统解耦、任务削峰等轻量级场景,比如:异步发送短信、订单通知、注册后欢迎邮件等。
但需要注意的是,Redis 并不是专业的消息中间件,它在以下方面存在不足:
- 消息丢失风险:比如消费者异常宕机、未及时处理消息,消息可能无法恢复;
- 缺乏完整的消费确认机制(list 模型下尤其明显);
- 不支持消息重试、死信队列等机制;
- 在极端高并发、海量消息场景下可扩展性和稳定性不如 Kafka、RocketMQ 等专业方案。
因此,实际选择时要根据业务场景权衡:
- 如果只是简单地做异步任务或削峰处理,对可靠性要求不高,Redis 是一种高效、灵活的方案;
- 如果消息的顺序、可靠投递、消息持久化等是关键要求,建议使用 Kafka、RabbitMQ 等成熟的消息中间件。
redis做搜索引擎
Redis 本身不是为搜索引擎设计的,但在一定程度上可以支持一些简单的搜索功能,特别是基于关键字的检索。通过手动构建倒排索引、结合集合操作(如 SINTER
、SUNION
)或使用 Redis Module(如 RedisSearch),可以实现关键词匹配、标签筛选等轻量级搜索能力。
例如,RedisSearch 模块提供了分词、全文索引、权重评分、排序等能力,支持类似搜索引擎的查询语法,适合构建中小型、对实时性要求高的搜索系统。
对于比较复杂或者数据规模较大的搜索场景,还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题:
数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。
分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。
聚合功能较弱:Elasticsearch 提供了丰富的聚合功能,而 RediSearch 的聚合功能相对较弱,只支持简单的聚合操作。
生态较差:Elasticsearch 可以轻松和常见的一些系统/软件集成比如 Hadoop、Spark、Kibana,而 RedisSearch 则不具备该优势。
RediSearch相对于Elasticsearch的优势:
性能更优秀:依赖 Redis 自身的高性能,基于内存操作(Elasticsearch 基于磁盘)。
较低内存占用实现快速索引:RediSearch 内部使用压缩的倒排索引,所以可以用较低的内存占用来实现索引的快速构建。
Redis 实现延时任务
基于 Redis 实现延时任务的功能有下面两种方案:
- Redis 过期事件监听。
- Redisson 内置的延时队列。
Redis 过期事件监听存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
Redis 过期事件监听如何实现延时任务
Redis 可以通过过期事件监听机制来实现延时任务。这个机制的核心原理是利用 Redis 的键空间通知(Keyspace Notifications)功能,当一个设置了过期时间的 key 到期时,Redis 会自动将其删除,并向指定频道发布一条过期事件通知。
具体来说,Redis 会将过期事件以消息的形式发送到名为 __keyevent@<db>__:expired
的频道(其中 <db>
是数据库编号)。我们只需要在客户端订阅这个频道,就能在 key 过期的瞬间感知到这个事件,并触发对应的业务逻辑,从而实现延时任务的调度。
这个机制本质上是 Redis 发布订阅(Pub/Sub)功能的一种内置应用场景。通过设置过期 key + 订阅过期事件,就可以在 key 被删除时执行延迟任务逻辑。
Redis 过期事件监听实现延时任务的缺陷
1、时效性差:过期事件消息是在 Redis 服务器删除 key 时发布的,而不是一个 key 过期之后就会就会直接发布。Redis 采用的是 定期删除+惰性删除 。因此,就会存在我设置了 key 的过期时间,但到了指定时间 key 还未被删除,进而没有发布过期事件的情况。
2、丢消息:Redis 的 pub/sub 模式中的消息并不支持持久化,这与消息队列不同。在 Redis 的 pub/sub 模式中,发布者将消息发送给指定的频道,订阅者监听相应的频道以接收消息。当没有订阅者时,消息会被直接丢弃,在 Redis 中不会存储该消息。
3、多服务实例下消息重复消费:Redis 的 pub/sub 模式目前只有广播模式,这意味着当生产者向特定频道发布一条消息时,所有订阅相关频道的消费者都能够收到该消息。这个时候,我们需要注意多个服务实例重复处理消息的问题,这会增加代码开发量和维护难度。
Redisson 延时队列
Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。
Redisson 定期使用 zrangebyscore
命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被消费者监听到。这样做可以避免消费者对整个 SortedSet 进行轮询,提高了执行效率。
Redisson 延时队列的优势
- 减少了丢消息的可能:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。
- 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。
跟 Redisson 内置的延时队列相比,消息队列可以通过保障消息消费的可靠性、控制消息生产者和消费者的数量等手段来实现更高的吞吐量和更强的可靠性,实际项目中首选使用消息队列的延时消息这种方案。
数据类型
5种基本数据类型
Redis 5 种基本数据类型其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
实现如下表所示:
String | List | Hash | Set | Zset |
---|---|---|---|---|
SDS | LinkedList/ZipList/QuickList | Dict、ZipList | Dict、Intset | ZipList、SkipList |
Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList。从 Redis 7.0 开始, ZipList 被 ListPack 取代。
String(字符串)
Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(Simple Dynamic String,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
SDS 的结构中包含了几个重要部分:
- len:记录当前已使用的字节数(不包含结尾的 \0),避免每次求长度都要遍历;
- alloc:表示分配的总容量,便于后续扩容;
- flags:标记 SDS 的类型;
- buf[]:存放实际的字符串内容,以
\0
结尾,兼容 C 字符串。
SDS 相比于 C 语言中的字符串有如下提升:
- 可以避免缓冲区溢出:C 语言中的字符串被修改(比如拼接)时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据 len 属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。
- 获取字符串长度的复杂度较低:C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取 len 属性即可,时间复杂度为 O(1)。
- 减少内存分配次数:为了避免修改(增加/减少)字符串时,每次都需要重新分配内存(C 语言的字符串是这样的),SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,Redis 会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。
- 二进制安全:C 语言中的字符串以空字符
\0
作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用 len 属性判断字符串是否结束,不存在这个问题。
应用场景:
需要存储常规数据的场景:缓存 Session、Token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。
需要计数的场景:用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。
分布式锁:利用
SETNX key value
命令可以实现一个最简易的分布式锁(存在一些缺陷,通常不建议这样实现分布式锁)。
List(列表)
Redis 中的 List 其实就是链表数据结构的实现。Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
应用场景:
信息流展示:最新文章、最新动态。
消息队列:
List
可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
Hash(哈希)
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象。
应用场景:
- 对象数据存储场景:举例:用户信息、商品信息、文章信息、购物车信息。
Set(集合)
Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一。
应用场景:
- 需要存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是
HyperLogLog
更适合一些)、文章点赞、动态点赞等场景。 - 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。
- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等场景。
Sorted Set(有序集合)
Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score
,使得集合中的元素能够按 score
进行有序排列,还可以通过 score
的范围来获取元素的列表。
Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树?
应用场景:
- 需要随机获取数据源中的元素根据某个权重进行排序的场景:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
- 需要存储的数据有优先级或者重要程度的场景 比如优先级任务队列
其他
存储对象数据用 String 还是 Hash 更好?
如果对象是整体读写,且字段变化不频繁,可以直接将对象序列化为一个字符串,用 Redis 的 String 类型存储。它结构简单、访问效率高,适合存储结构固定、内容完整读取的场景,比如用户信息、配置快照等。
而使用 Hash 类型,则更适合对象字段粒度更细、需要频繁按字段读写的情况。Redis 会将 Hash 中的每个 field-value 对独立管理,支持单字段更新,节省流量,也更利于数据解耦。例如,修改用户昵称、头像时只需更新对应字段,无需整体序列化或反序列化。
此外,Hash 类型在字段较少时内部实现是压缩结构,占用内存更小,但字段过多时可能会影响性能。
- 如果是整体读写、结构固定,适合用 String;如果需要按字段读写、字段较多、变更频繁,Hash 更灵活高效。
购物车信息用 String 还是 Hash 存储更好呢?
存储购物车信息,更推荐使用 Hash 类型,因为它更适合按用户维度分组、按商品维度操作的场景。
具体来说,购物车的数据结构通常是:一个用户对应多个商品及其数量。使用 Hash 类型,我们可以将每个用户的购物车作为一个 key(如 cart:userId
),其中 field 是商品 ID,value 是数量。
这样的设计带来几个优势:
- 按需更新字段:可以只修改某个商品的数量,避免整体序列化和反序列化;
- 结构清晰:每个用户一份 cart,Redis key 总量可控,便于管理;
- 节省空间:Redis 对小规模 Hash 做了压缩编码处理,内存更高效;
- 业务操作灵活:可以通过
HGETALL
快速获取整个购物车,或者用HINCRBY
修改某一商品数量。
相比之下,如果使用 String 存储,每个用户的购物车都需要整体序列化成字符串(如 JSON),每次修改都涉及反序列化、修改、再写入,性能开销更大,代码也更复杂。
使用Sorted Set实现排行榜
Redis 实现排行榜最常用的数据结构是 有序集合(Sorted Set),它天生支持按分数排序,非常适合排行榜这种按成绩、积分、热度排名的场景。
在实现上,排行榜中的每个用户或对象作为有序集合的 member,得分作为 score。我们可以通过以下方式实现:
- 添加或更新成员排名:使用
ZADD
命令将用户及其分数加入排行榜,已有则更新; - 获取某个范围的排名:使用
ZREVRANGE
实现从高到低获取前 N 名; - 获取某个用户的排名:使用
ZREVRANK
获取某个用户当前在排行榜中的名次; - 获取某个用户的得分:使用
ZSCORE
查看分数; - 按需设置过期时间或定期清理:可按天、周、月维护多个排行榜 key,例如
rank:daily:20250716
,便于隔离不同周期的数据。
Redis 的有序集合底层使用跳表实现,支持按分数排序的高效插入与查询操作,同时保持集合中成员唯一,避免重复用户。
使用 Set 实现抽奖系统
Redis 的 Set 类型非常适合用来实现抽奖系统,原因是它具有元素唯一性和随机操作能力,可以高效地完成抽奖相关逻辑。
具体实现方式如下:
- 初始化奖池:将所有参与抽奖的用户 ID 或奖品 ID 存入一个 Set,例如
SADD lottery_users user1 user2 ...
; - 随机抽取中奖者:使用
SRANDMEMBER
从 Set 中随机抽取指定数量的元素,但不删除; - 抽中后移除:如果抽奖规则要求不能重复中奖,可以使用
SPOP
,该命令会从 Set 中随机弹出元素,抽一次减一个; - 查看当前奖池人数:使用
SCARD
查看当前参与抽奖的总人数; - 防止重复参与:Set 的唯一性特性天然防止重复添加用户;
- 记录中奖名单:可以将中奖者另存一个 Set 或 List,用于后续展示或发奖。
这种方式适用于用户数量中等、并发不高的抽奖系统,数据结构简单,效率高。
3种特殊数据类型
Bitmap(位图)
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。
应用场景:
- 需要保存状态信息(0/1 即可表示)的场景:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
HyperLogLog (基数统计)
HyperLogLog 是一种有名的基数计数概率算法 ,基于 LogLog Counting(LLC)优化改进得来,并不是 Redis 特有的,Redis 只是实现了这个算法并提供了一些开箱即用的 API。
Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64
个不同元素。并且,Redis 对 HyperLogLog 的存储结构做了优化,采用两种方式计数:
- 稀疏矩阵:计数较少的时候,占用空间很小。
- 稠密矩阵:计数达到某个阈值的时候,占用 12k 的空间。
基数计数概率算法为了节省内存并不会直接存储元数据,而是通过一定的概率统计方法预估基数值(集合中包含元素的个数)。因此, HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 0.81%
)。
应用场景:
- 数量巨大(百万、千万级别以上)的计数场景:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。
Geospatial index(地理位置)
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
Redis 到底是怎么实现“附近的人”这个功能的呢?前言:针对“附近的人”这一位置服务领域的应用场景,常见的可使用PG、 - 掘金
应用场景:
- 需要管理使用地理空间数据的场景:附近的人。
其他
使用 Bitmap 统计活跃用户
使用 Redis 的 Bitmap 可以非常高效地统计用户的活跃状态,尤其适合按天、按月等周期统计用户是否活跃、活跃人数等场景,核心思路是:用用户 ID 对应 Bitmap 的偏移位,值为 1 表示当天活跃,0 表示未活跃。
具体做法如下:
- 设置活跃状态:当某个用户访问系统时,使用
SETBIT key userId 1
将对应偏移位置为 1。例如:SETBIT active:20250716 12345 1
,表示用户 ID 为 12345 在 7 月 16 日活跃; - 统计活跃人数:使用
BITCOUNT key
统计 Bitmap 中值为 1 的位数,即当天活跃用户数; - 判断某用户是否活跃:用
GETBIT key userId
判断某天某用户是否活跃; - 多天活跃分析:可使用
BITOP AND/OR
对多个日期的 Bitmap 做并集或交集,分析连续活跃、总活跃人数等; - 空间高效:Bitmap 本质是二进制位图,支持千万级用户状态压缩在少量内存中,非常节省空间。
这种方式适合大规模用户活跃统计,尤其在日活、周活、留存分析等业务中非常常见。
使用 HyperLogLog 统计页面 UV
HyperLogLog 是 Redis 提供的一种基于概率的数据结构,专门用于高性能地统计海量数据的基数(即不重复元素的个数),非常适合用来统计网站页面的 UV(Unique Visitor)。
实现方式如下:
- 记录访客:每当用户访问页面时,使用
PFADD key userId
将用户 ID(如 IP、用户 ID、会话 ID)添加到 HyperLogLog 中。例如:PFADD uv:20250716 12345
; - 获取 UV 数:使用
PFCOUNT key
获取 HyperLogLog 中去重后的用户数量,即当天页面 UV; - 跨日合并统计:若需统计某段时间内的 UV,比如一周,可用
PFMERGE
将多日数据合并到一个新 key,再用PFCOUNT
查询; - 空间高效:HyperLogLog 不存储具体用户 ID,只维护概率桶,内存占用恒定在约 12KB,即使统计上亿用户,空间也不会增加。
需要注意的是,HyperLogLog 是一种近似统计结构,误差在 0.81% 左右,不适用于要求精确去重的场景。
持久化机制
Redis 支持持久化,而且支持 3 种持久化方式:
- 快照(snapshotting,RDB);
- 只追加文件(append-only file,AOF);
- RDB 和 AOF 的混合持久化(Redis 4.0 新增)。
RDB持久化
Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是 Redis 默认采用的持久化方式
RDB 创建快照时会阻塞主线程吗?
Redis 提供了两个命令来生成 RDB 快照文件:
save
: 同步保存操作,会阻塞 Redis 主线程;bgsave
: fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
AOF持久化
与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了)。
工作流程
AOF 持久化功能的实现可以简单分为 5 步:
- 命令追加(append):所有的写命令会追加到 AOF 缓冲区中。
- 文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用
write
函数(系统调用),write
将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。 - 文件同步(fsync):AOF 缓冲区根据对应的持久化方式(
fsync
策略)向硬盘做同步操作。这一步需要调用fsync
函数(系统调用),fsync
针对单个文件操作,对其进行强制硬盘同步,fsync
将阻塞直到写入磁盘完成后返回,保证了数据持久化。 - 文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
其他
线程模型
对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作,Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
单线程模型
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型(Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
文件事件处理器
在 Redis 中,文件事件处理器(File Event Handler)是其事件驱动架构的核心组件,用于协调客户端请求的接收、处理和响应,是实现高并发网络通信的关键机制。
可以简单理解为以下四个部分组成:
- 多路复用器(IO 多路复用) Redis 底层使用
select
、epoll
等系统调用(Linux 默认是 epoll)监听多个 socket 上的事件(如可读、可写、新连接等)。 - 文件事件(File Events) 每个 socket(客户端连接)上的“读就绪”“写就绪”事件被封装成文件事件。
- 事件分派器 Redis 主线程不断从多路复用器中获取就绪事件,并将其分派给对应的事件处理器函数(回调函数)去处理。
- 事件处理器 比如处理“读事件”时,会读取客户端发送的命令,再执行命令并准备返回结果。
单线程怎么监听大量的客户端连接呢?
虽然 Redis 的命令处理是单线程的,但它依然可以高效处理大量客户端连接,关键在于它采用了I/O 多路复用技术。
具体来说,Redis 使用的是 epoll
(Linux 下的高性能 I/O 多路复用机制),通过一个线程同时监听多个客户端的 socket 连接事件。当某个连接有数据可读时,Redis 就会在主线程中依次读取、解析并执行命令。
这种方式的优点是:
- 不需要为每个连接创建线程,避免了线程上下文切换和资源开销;
- 利用事件驱动机制,让主线程始终处于高效的事件循环中,只处理“就绪”的连接;
- 即使有成千上万个连接,只要不是同时大量发送命令,Redis 依旧能快速响应。
从 Redis 6.0 开始,还引入了多线程处理网络读写,进一步缓解了单线程 I/O 的瓶颈,但命令执行仍然在主线程中,保证了数据操作的原子性和一致性。
为什么没有使用多线程
Redis 在 6.0 之前没有使用多线程,是出于性能、简单性和一致性的考虑。Redis 以“单线程+事件驱动”著称,这种模型的优点是:
- 避免了并发控制的复杂性:操作完全串行,无需加锁,天然线程安全;
- 延迟更低、执行更快:内存操作快,加上 I/O 多路复用,不需要频繁上下文切换;
- 代码逻辑清晰,易于维护和调优。
在当时的硬件和业务场景下,这种设计足够支撑大多数中高并发需求。
多线程模型
为什么引入多线程
是因为在高并发网络场景下,瓶颈不再是命令执行,而是网络 I/O 和数据读写。Redis 的单线程模型虽然执行快,但面对大量并发连接时:
- 读取客户端数据(recv)、解析命令、发送响应(send)这些 I/O 操作耗时增加;
- 主线程需要处理的工作变重,CPU 利用率不均衡,多核优势无法发挥;
- 在网卡和网络栈优化之后,网络成为了新的性能短板。
因此,从 Redis 6.0 开始,引入了多线程用于网络读写阶段,即:
- 多个线程负责接收客户端请求、读取数据;
- 命令解析和执行仍由主线程处理,保持数据一致性和简洁性;
- 响应发送也可通过多线程加速。
这种方式既提升了网络性能,又不引入多线程并发执行命令的复杂性。
后台线程
虽然 Redis 的命令处理主流程是单线程的,但它实际上运行着多个后台线程或子线程,用于处理一些耗时操作或系统性任务,以避免阻塞主线程,提高整体响应能力。
常见的后台线程或任务包括:
- 持久化相关线程
- Redis 在执行 RDB 快照或 AOF 重写时,会创建子进程(fork),用于在后台保存数据,不影响主线程服务客户端请求。
- 异步删除大 key 或释放内存
- 当删除大型对象(如大 List/Hash)或键空间淘汰时,Redis 会将这些操作交给后台线程异步处理,防止主线程被长时间阻塞。
- AOF rewrite buffer 同步线程
- 在 AOF 重写过程中,有专门线程将命令追加到重写缓冲区。
- I/O 多线程(从 Redis 6.0 起)
- Redis 引入了多线程支持用于处理网络读写,主线程负责命令执行,多线程负责数据的接收和发送,提高并发性能。
- 后台定期任务线程
- 包括过期键删除、定时任务、统计信息更新等,也会部分通过异步调度方式处理,避免集中耗时。
内存管理
数据过期
一般情况下,缓存数据都会设置一个过期时间。为什么?
缓存数据设置过期时间,主要是为了解决数据一致性、内存占用和系统稳定性等几个核心问题:
- 保证数据的时效性和一致性 缓存属于副本,可能会和数据库中的真实数据存在差异。设置过期时间可以让旧数据自动失效,降低缓存与数据源之间的不一致风险。
- 防止缓存膨胀,节约内存资源 如果不设置过期时间,缓存中的数据可能长期存在,导致内存不断增长,最终可能引发 OOM(内存溢出)或缓存淘汰压力过大。
- 提升系统的可控性和可维护性 设置合理的过期策略,可以更好地控制缓存命中率、失效时机,从而在更新频繁、热点突变等场景下保持系统稳定。
- 应对业务场景中的数据变化 比如排行榜、活动页、热搜词等数据本身就是周期性变化的,设置过期时间可以自动驱动数据刷新,减少手动干预。
Redis 是如何判断数据是否过期的呢?
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
当 Redis 需要判断一个 key 是否过期时,会执行以下逻辑:
**1. 先查主字典(dict):**是否存在这个 key; **2. 再查过期字典(expires dict):**如果存在该 key 的过期时间,Redis 会将当前系统时间与其过期时间进行对比; 3. 如果当前时间 ≥ 过期时间,则认为该 key 已过期。
大量Key集中过期怎么办
当 Redis 中存在大量 key 在同一时间点集中过期时,可能会导致以下问题:
- 请求延迟增加:Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。
- 内存占用过高:过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。
为了避免这些问题,可以采取以下方案:
- 尽量避免 key 集中过期:在设置键的过期时间时尽量随机一点。
- 开启 lazy free 机制:修改
redis.conf
配置文件,将lazyfree-lazy-expire
参数设置为yes
,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。
删除策略
常用的过期数据的删除策略:
- 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。
- 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
- 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。
Redis 采用的是那种删除策略呢?
Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。
为什么 Redis 采用惰性删除和定期删除的策略,而不是在 key 一过期就立即删除它?
主要是出于性能和可扩展性的考虑。
首先,如果要做到 key 一过期就立刻删除,就意味着 Redis 需要为每一个设置了过期时间的 key 启动一个定时器,持续监听它是否到期。这种方式会带来两个严重的问题:
- 性能开销高 Redis 是高性能的内存数据库,如果每个 key 都要创建定时任务或轮询检查,会造成 CPU 资源浪费,尤其在大量 key 存在时,系统开销难以控制。
- 线程调度复杂 Redis 的核心是单线程执行命令,频繁地处理定时删除会引入大量异步调度逻辑,破坏简单的事件模型,增加系统复杂度,也容易引发不可控的延迟。
Redis 中的定期删除是如何做的
它的实现方式并不是遍历所有设置了过期时间的 key,而是采用了“定期 + 随机 + 控制频率”的策略,具体逻辑如下:
- 执行频率 默认每秒执行 10 次过期扫描(由
serverCron
定时器触发)。 - 随机抽样 每次从带有过期时间的 key 中随机抽取最多 20 个 key。
- 过期判断并删除 遍历这 20 个 key,比较当前时间与每个 key 的过期时间,如果已过期则立即删除。
- 重复触发机制 如果这 20 个 key 中,超过 25% 是已过期的,Redis 会继续执行下一轮扫描,直到比例小于 25% 或达到最大执行时间限制(避免阻塞主线程)。
为什么是随机抽样而不是把所有过期 key 都删除?
这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。
内存淘汰策略
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过 redis.conf
的 maxmemory
参数来定义的。64 位操作系统下,maxmemory
默认为 0,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。
Redis 提供了多种内存淘汰策略,用于在内存使用达到上限时决定如何处理新写入请求。整体策略分为两类:只淘汰设置了过期时间的 key,和对所有 key 都可能淘汰。
具体来说,有以下几种常见策略:
- noeviction:默认策略,不淘汰任何 key,一旦内存满了,写入命令会报错,适用于对数据完整性要求高的场景。
- volatile-lru / allkeys-lru:使用最近最少使用(LRU)策略淘汰 key,
volatile-lru
只针对设置了过期时间的 key,allkeys-lru
则适用于所有 key,适合缓存类应用。 - volatile-lfu / allkeys-lfu:使用最不常用(LFU)策略淘汰 key,淘汰访问频率最低的 key,更适合访问分布稳定的场景。
- volatile-random / allkeys-random:随机淘汰 key,策略简单但效果不稳定。
- volatile-ttl:优先淘汰快要过期的 key,基于剩余生存时间。
内存碎片
为什么redis会有内存碎片
Redis 内存碎片的本质,是由于内存分配和释放不均衡,导致物理内存中虽然还有空闲空间,但无法有效利用,形成内存碎片。
主要原因包括以下几点:
- 频繁分配与释放内存: Redis 在不断写入、删除 key 的过程中,频繁地申请和释放内存,容易造成大小不一的空闲块分布在内存中,难以复用。
- 数据结构扩容缩容: 像 Hash、List、Set 等结构在元素变化时会动态扩容或缩容,旧空间释放,新空间分配,容易产生碎片。
- 内存对齐与分配算法的限制: Redis 底层使用 jemalloc 等内存分配器,为了对齐性能,可能申请比实际需要更大的内存空间,留下难以复用的小空洞。
- 惰性删除和过期清理导致空间不连续释放: 一些 key 被延迟删除,导致释放内存的时间不集中,增加碎片化概率。
事务
Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
鸡肋,使用的少并且不建议使用
原子性
Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
持久性
Redis 支持持久化,而且支持 3 种持久化方式:
- 快照(snapshotting,RDB);
- 只追加文件(append-only file,AOF);
- RDB 和 AOF 的混合持久化(Redis 4.0 新增)。
简单来说:
- 如果开启了 AOF 持久化,Redis 会在执行事务中的每条命令时写入 AOF 文件;
- 如果依赖的是 RDB 快照,则要等到下一次快照发生时数据才会被持久化;
- 如果两种机制都未启用,那么事务执行的数据在 Redis 重启后会丢失。
因此,Redis 的事务可以结合持久化机制实现“持久性”,但事务本身不等同于数据库那种强 ACID 保证。
性能优化
穿透击穿雪崩阻塞
缓存穿透
缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决方法
常见的解决方案有三种:
第一,缓存空值。 当请求的 key 在数据库中查不到时,将一个空对象或特定标识(如 null
或 ""
)写入缓存,并设置较短的过期时间。这样后续相同请求就不会再打到数据库。
第二,使用布隆过滤器。 将所有合法 key 的集合预先存入布隆过滤器,每次请求前先判断 key 是否存在于布隆过滤器中。如果判断为不存在,直接拦截请求,避免访问缓存和数据库。
第三,加强接口安全。 通过接口校验、用户权限控制、验证签名、加验证码等方式,限制恶意用户或机器人批量构造非法请求。
缓存击穿
缓存击穿是指某个热点 key 在缓存中刚好失效的瞬间,有大量并发请求同时访问这个 key,由于缓存失效,请求全部穿透到数据库,造成瞬时高并发压力,可能引发数据库崩溃。
解决方法
第一,加互斥锁。 当发现某个 key 失效时,让第一个请求线程去加载数据库并重建缓存,其他线程等待或快速失败。常见做法是在查询时对 key 加分布式锁,避免重复加载。
第二,设置合理的过期时间 + 提前异步刷新。 给热点 key 设置较长过期时间,同时使用定时任务或异步线程,在临近过期时主动刷新缓存,避免大面积失效。
第三,永不过期 + 后台更新机制。 对一些真正高频访问的数据,可以考虑缓存永久有效,由后台异步服务定时更新缓存,完全避免过期瞬间失效。
缓存雪崩
存雪崩是指大量缓存数据在同一时间集中失效,导致大量请求同时访问数据库,数据库承压严重,可能宕机。这种问题一般发生在大量 key 过期时间相同,或者 Redis 故障时。
解决方法
针对 Redis 服务不可用的情况:
- Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案。
- 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
针对大量缓存同时失效的情况:
- 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
- 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
阻塞
为什么阻塞?
尽管 Redis 是基于单线程模型,处理速度非常快,但仍会因为某些操作耗时过长而阻塞主线程。常见原因有:
- 大 key 操作 对包含大量元素的 key 执行全量命令(如
LRANGE
、HGETALL
、SMEMBERS
)会导致长时间占用主线程。 - 慢查询命令 一些命令本身计算复杂,如
SORT
、ZUNIONSTORE
、SUNION
,对数据量大的 key 会严重拖慢响应。 - 阻塞型命令 命令如
BLPOP
、BRPOP
在 key 不存在时会阻塞客户端,客户端线程被挂起等待数据返回,但不会阻塞 Redis 本身。 - AOF 重写 / RDB 快照期间资源竞争 虽然是后台子进程执行,但若服务器 I/O 或 CPU 资源紧张,会影响主线程性能,引发间接阻塞。
- 客户端读写缓冲区积压 如果客户端消费能力差,写缓冲区过大,Redis 主线程需要等待数据写完才能继续处理请求,导致延迟。
- 主从复制阻塞 全量同步或从库处理慢会影响主库
PSYNC
、BGSAVE
等操作,影响主线程响应。
如何解决阻塞问题?
应对 Redis 阻塞问题,通常从优化使用场景、命令选择、资源管理几方面入手:
- 避免操作大 key
- 拆分大 key 为多个小 key
- 禁用高风险命令,限制 key 大小
- 使用
SCAN
替代全量读取
- 优化慢查询命令
- 尽量避免使用全量计算类命令
- 利用分页处理、预计算等手段
- 合理使用阻塞命令
- 设置合理超时时间
- 对关键场景使用消息队列替代阻塞命令
- 优化持久化策略
- 在流量低峰期执行 AOF 重写或 RDB 快照
- 开启
lazy-free
异步删除大 key
- 增强监控与告警
- 使用
slowlog
、info
、latency
命令跟踪瓶颈 - 对慢操作设置告警,提前定位问题
- 使用
- 隔离关键业务或分片部署
- 高并发或高吞吐场景下进行 Redis 分区分库
- 核心数据与非核心数据分离部署,减少干扰
集群
Redis Sentinel:
- 什么是 Sentinel? 有什么用?
- Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
- Sentinel 是如何实现故障转移的?
- 为什么建议部署多个 sentinel 节点(哨兵集群)?
- Sentinel 如何选择出新的 master(选举机制)?
- 如何从 Sentinel 集群中选择出 Leader?
- Sentinel 可以防止脑裂吗?
Redis Cluster:
- 为什么需要 Redis Cluster?解决了什么问题?有什么优势?
- Redis Cluster 是如何分片的?
- 为什么 Redis Cluster 的哈希槽是 16384 个?
- 如何确定给定 key 的应该分布到哪个哈希槽中?
- Redis Cluster 支持重新分配哈希槽吗?
- Redis Cluster 扩容缩容期间可以提供服务吗?
- Redis Cluster 中的节点是怎么进行通信的?
其他
缓存与数据库双写时存在数据不一致的风险,尤其在更新、删除操作中。常见的一致性方案有以下几种:
第一,先删除缓存,再更新数据库。 这是较常用的方案,更新数据前先删掉缓存,等下次读请求时再从数据库加载最新数据写入缓存。但这存在并发问题:如果删除缓存后还没来得及更新数据库,读取请求进来了,就会缓存旧数据。
第二,先更新数据库,再删除缓存。 这是更推荐的做法,更新数据库成功后再删除缓存,避免缓存中存在过期脏数据。为了防止并发问题,可以结合延迟双删策略:第一次删除在更新后立即执行,第二次删除在短暂延迟后异步执行一次,确保彻底清理脏缓存。
第三,使用消息队列异步更新缓存。 将数据库更新操作投递到消息队列,由消费端统一负责删除或更新缓存,提高系统解耦性和可靠性。
第四,引入 Canal 等中间件监听数据库变更。 通过监听数据库 binlog,实时感知变更并同步更新缓存,适合数据一致性要求高的场景。