在 Redis 基础详解(上篇)介绍了 redis 的基本使用,本文将深入探索 redis 还有哪些常见知识。
1、Redis的基本数据结构类型
1. String(字符串)
可以是文本、数字(可自增自减)或二进制数据,最大能存储 512MB。
适合的业务场景:
- 缓存:最经典的用法。缓存数据库查询结果、网页内容、会话信息等。例如:
SET user:1001 "{name: 'Alice', email: 'alice@example.com'}"。 - 计数器:利用
INCR和DECR命令实现原子性操作,无需担心多线程竞争。例如:文章阅读量INCR article:100:views、网站总用户数、限流器(每秒请求数)。 - 分布式锁:利用
SET key value NX EX seconds命令实现简单的分布式锁。NX表示只有当 Key 不存在时才设置,EX设置过期时间。
2. Hash(哈希/字典)
将多个 Field-Value 对存储在一个 Redis Key 中。适合表示一个对象。
适合的业务场景:
- 存储对象信息:这是 Hash 的最佳场景。例如存储用户信息
HMSET user:1001 name "Alice" age "30" email "alice@example.com"。相比于将整个用户对象序列化成 String 存储,Hash 允许单独获取或更新某一个字段,更加高效,节省网络带宽。 - 购物车:以用户ID为 Key,商品ID为 Field,商品数量为 Value。例如:
HSET cart:1001 商品A 2,HINCRBY cart:1001 商品A 1。可以非常方便地添加商品、增加数量、获取所有商品。
3. List(列表)
一个简单的字符串列表,按插入顺序排序,可以从列表的头部或尾部添加元素。有序、可重复,支持双向操作。
适合的业务场景:
- 消息队列(简单版):使用
LPUSH(生产消息)和BRPOP(阻塞消费消息)可以实现一个简单的 FIFO 队列。但由于缺乏 ACK 机制和消息重试,对于要求严格的业务,更推荐使用 Stream。 - 最新消息/文章列表:例如朋友圈的时间线、新闻推送。
LPUSH添加新内容,LRANGE 0 9获取最新的10条。 - 记录操作日志:将用户的操作记录
LPUSH到一个列表中,需要时可以从头遍历。
4. Set(集合)
元素无序且不可重复,支持高效的集合运算(交集、并集、差集)。
适合的业务场景:
- 标签(Tag)系统:给文章、用户添加标签。
SADD article:1001:tags tech redis python。可以轻松找出拥有共同标签的文章(求交集)。 - 共同关注/好友:
SINTER user:1001:follows user:1002:follows可以快速计算出两个用户的共同关注。 - 抽奖/随机推荐:
SRANDMEMBER或SPOP命令可以随机返回一个或多个元素,非常适合实现抽奖活动。 - 数据去重:确保添加的元素不重复,例如对一批爬取的 URL 进行去重。
5. Sorted Set(有序集合 / ZSet)
元素唯一,有排序功能,为每个元素关联了一个 score(分数),元素按 score 进行从小到大排序。元素是唯一的,但 score 可以重复。。
适合的业务场景:
- 排行榜:这是最完美的应用场景。例如游戏积分排行榜、热搜榜。
ZADD leaderboard 100 "playerA",ZINCRBY leaderboard 5 "playerA"增加积分,ZREVRANGE leaderboard 0 9 WITHSCORES获取前十名。 - 带权重的队列:
score可以代表任务的优先级,消费者按score顺序来处理任务。 - 范围查找:例如处理按时间排序的数据。将时间戳作为
score,ZRANGEBYSCORE可以轻松查询某一时间段内的数据。
6. HyperLogLog(基数统计)
用于做基数统计(不重复元素的个数)的算法。它的优点是,输入元素的数量或体积非常大时,计算基数所需的空间总是固定且很小。
适合的业务场景:
- 大规模数据去重统计:最典型的就是网站 UV(独立访客数)统计。统计一天内访问某个页面的不同 IP 地址数。如果你用 Set 存储所有 IP,数据量大会非常耗内存。而使用 HyperLogLog,
PFADD uv:20231027 192.168.1.1,最后PFCOUNT uv:20231027,只需 12KB 内存就能完成亿级数据的统计,且可以接受微小误差。
7. Geo(地理空间)
专门用于存储和操作地理位置信息的数据结构。可以存储经纬度,并计算两地距离、查找指定范围内的地点等。
适合的业务场景:
- 附近的人/地点:例如“查找我半径 1 公里内的所有餐馆”、“共享单车附近的车辆”。
GEOADD locations 116.405285 39.904989 "Beijing",然后GEORADIUS locations 116.40 39.90 100 km查找附近100km的地点。 - 计算距离:
GEODIST locations Beijing Shanghai km计算北京和上海之间的直线距离。
2、Redis 为什么这么快?
基于内存存储实现
这是 最根本、最主要 的原因。所有传统数据库(如 MySQL)的数据都存储在硬盘上。磁盘 I/O 是计算机操作中最慢的环节之一,其速度通常在毫秒(ms)级别。而内存的访问速度是纳秒(ns)级别。这意味着内存的访问速度比磁盘快 10万倍 以上。
Redis 将所有数据放在内存中,直接绕过了这个最慢的瓶颈,使得读写操作变得极其迅速。也正因为数据在内存中,所以 Redis 的存储容量受限于主机内存的大小,这也是为什么 Redis 通常不用作主数据库,而作为缓存或存储非海量热数据的原因。单线程模型
Redis是单线程模型的,而单线程避免了 CPU 不必要的上下文切换和竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如hgetall命令),会造成阻塞。I/O 多路复用 (I/O Multiplexing)
这是 Redis 单线程却能处理数万并发连接的核心技术。
Redis 使用单线程的 Reactor 模式。通过epoll等系统调用,由一个线程监听大量 socket 连接。当某个 socket 有事件产生(如可读、可写)时,epoll 会通知主线程,主线程再依次处理这些事件。用一个线程就管理了成千上万的网络连接,极大地减少了资源消耗,使得系统能够轻松应对高并发请求。网络 I/O 的瓶颈不再成为性能限制。
3、缓存击穿、缓存穿透、缓存雪崩
缓存穿透、缓存雪崩和缓存击穿是分布式系统设计中非常经典且必须掌握的三个问题。它们都描述了缓存失效导致请求直接压到数据库,可能引发数据库崩溃的场景,但成因和解决方案各有不同。
3.1、缓存穿透 (Cache Penetration)
缓存穿透是指查询一个根本不存在的数据。由于缓存中不存在(未写入),数据库中也不存在,因此这个请求每次都会绕过缓存,直接查询数据库。如果有人恶意大量发起这类请求,就会给数据库造成巨大压力甚至压垮数据库。
原因:
- 业务代码逻辑漏洞。
- 恶意攻击:黑客故意发起大量请求查询不存在的商品ID、用户ID等。
解决方案:
-
缓存空对象 (Cache Null Object):
- 当数据库中也查询不到数据时,我们仍然将这个空结果(例如
null)进行缓存,并设置一个较短的过期时间(例如 5-10分钟)。 - 优点:实现简单,能有效应对短期攻击。
- 缺点:如果攻击者每次使用不同的不存在的Key,此方法效果会减弱,且会缓存大量无用的空数据。
- 当数据库中也查询不到数据时,我们仍然将这个空结果(例如
-
布隆过滤器 (Bloom Filter):
- 在缓存之前,设置一个布隆过滤器。
- 工作原理:布隆过滤器是一个很长的二进制向量和一系列随机映射函数。它可以高效地判断一个元素是否一定不存在于某个集合中。
-
流程:所有可能存在的key都预先存入布隆过滤器。当一个查询请求到来时:
- 先通过布隆过滤器判断 key 是否存在。
- 如果不存在,则直接返回空,不再查询缓存和数据库。
- 如果存在,则继续后续的缓存和数据库查询流程。
- 优点:内存占用极小,效率极高。
- 缺点:实现较为复杂;存在一定的误判率(但只会误判为“存在”,即可能放过一个不存在的key去查数据库,但绝不会错误地拦截一个存在的key);无法删除数据。
3.2、缓存击穿 (Cache Breakdown)
缓存击穿是指某一个热点key(访问量非常大的key) 在缓存过期的瞬间,同时有大量请求这个key的请求进来。由于缓存刚好失效,这些请求都会落到数据库上,瞬间的巨大并发可能会压垮数据库。
注意:这个key是存在的,只是在过期的那一刻成为了系统的一个脆弱点。
原因:
- 热点数据缓存过期。
解决方案:
-
设置热点数据永不过期:
- 对于极热点数据,可以从逻辑上设置为永不过期。但这并不意味着数据不更新。
- 由后台任务或代码逻辑在数据变更时主动更新缓存(
set操作),而不是等它自己过期。
-
互斥锁 (Mutex Lock):
- 当缓存失效时,不立即去 load db。
- 先使用缓存工具的某些带成功操作返回值的命令(如 Redis 的
SETNX或SET ... NX)去设置一个互斥锁。 - 当操作返回成功时,再进行 load db 的操作并回设缓存,最后删除互斥锁。
- 当操作返回失败(意味着其他线程已经去 load db 了),则当前线程休眠一段时间(例如100ms)后重试整个获取缓存的方法。
- 优点:能很好地保证一致性,防止大量请求穿透。
- 缺点:存在死锁风险;性能上有损耗。
3.3、缓存雪崩 (Cache Avalanche)
缓存雪崩是指大量的缓存key在同一时间段失效或者Redis缓存服务器直接宕机。导致所有原本应该访问缓存的请求都去查询数据库,导致数据库压力骤增甚至宕机,就像雪崩一样连锁反应。
缓存雪崩与缓存击穿的区别在于:击穿是针对单个热点key,雪崩则是针对大量key。
原因:
- 同一时间过期:例如,在批量存入缓存时,设置了相同的过期时间。
- Redis服务宕机:缓存层完全崩溃。
解决方案:
-
错开过期时间:
- 给缓存的过期时间加上一个随机值(例如,基础时间 + 随机1-5分钟)。
- 这样可以保证key不会在同一时间大面积失效,而是均匀分布。
-
构建高可用的缓存集群:
- 通过 Redis 哨兵(Sentinel) 或集群(Cluster) 模式,实现Redis服务的高可用。
- 这样即使个别Redis节点宕机,整个缓存层依然能正常运行,防止“服务宕机”导致的雪崩。
4. Redis 过期策略
在 set key 的时候,可以给它设置一个过期时间,如 expire key 60,指定 key 60s 后过期。60s 后 redis 是如何处理的?先来介绍几种过期策略。
1. 定时过期
每个有过期时间的 key 都需要创建一个定时器,到过期时间会立即对 key 进行清除。该策略可以立即清除过期的数据,对内存友好;但是会占用大量的 CPU 去处理过期的数据,从而影响缓存的响应时间和吞吐量。
2. 惰性过期
只有当访问一个 key 时,才判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU,却对内存不友好。极端情况可能出现大量的过期 key 没有被访问,从而不会被清除,占用大量内存。
3. 定期过期
每隔一定的时间,会扫描一定数量的 expires 字典中的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
5、Redis 的持久化机制
Redis 是基于内存的非关系型 K-V 数据库,既然它是基于内存的,如果 Redis 服务器挂了,数据就会丢失。为了避免数据丢失了,Redis提供了持久化,即把数据保存到磁盘。
Redis 提供了 RDB 和 AOF 两种持久化机制,它持久化文件加载流程如下:
5.1、RDB (Redis Database)
RDB持久化是通过创建某个时间点的数据快照(Snapshot) 来完成的。它产生的是一个经过压缩的二进制文件(默认名为 dump.rdb)。
优点:
- 性能高,恢复速度快:RDB文件是紧凑的二进制文件,写入和加载速度都非常快。非常适合用于灾难恢复(Disaster Recovery)和大规模数据重启恢复。
- 适合备份与容灾:由于文件是紧凑的,可以很方便地定时生成RDB文件并归档到远程数据中心或对象存储(如S3)中。
-
最大化Redis性能:父进程唯一需要做的就是
fork一个子进程来完成持久化工作,父进程继续处理客户端请求,对性能影响相对较小。
缺点:
-
数据安全性低,可能丢失数据:RDB是定时快照,如果Redis意外宕机,从上一次快照到宕机时的数据会全部丢失。即使在配置了
save 60 10000的情况下,也最多可能丢失60秒的数据。 -
fork()可能阻塞进程:虽然BGSAVE是后台执行,但fork子进程的瞬间,如果数据量非常大,fork操作本身可能会阻塞主进程(毫秒级或秒级),影响吞吐量。
5.1、AOF (Append Only File)
AOF持久化是通过记录每一次写操作命令 来完成的。它像一个日志文件,记录了对Redis数据状态的所有修改性命令。
优点:
-
数据安全性极高:根据
appendfsync策略,最多只会丢失一秒的数据(默认配置下),甚至可以不丢失任何数据(always)。 - 可读性:AOF文件是纯文本格式,记录了所有操作命令,便于理解和人工修复(不过一般不需要这么做)。
缺点:
- 文件体积大:AOF文件通常比同数据集的RDB文件大得多。
- 恢复速度慢:在Redis重启恢复数据时,需要逐条执行AOF文件中的所有命令,这个过程比加载RDB文件要慢很多。
-
对性能影响相对较大:虽然在
everysec配置下性能已经很好,但写入负载非常高时,AOF的吞吐量仍然可能略低于RDB。
6、怎么实现 Redis 的高可用
在生产环节使用 Redis,肯定不会是单点部署。单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。
Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。
6.1、主从复制 (Replication)
主从复制是 Redis 数据冗余和读写分离的基础。它允许一个 Redis 服务器(主节点,Master)将其数据复制到一个或多个 Redis 服务器(从节点,Slave/Replica)。
- 工作模式:一主多从。
- 数据流向:单向的,只能从主节点复制到从节点。
- 核心职责:数据同步。
如何工作?
- 从节点启动后,会向主节点发送
SYNC(全量同步)或PSYNC(部分同步,Redis 2.8+)命令。 - 主节点接收到命令后,会生成当前数据的快照(RDB文件) 并将其发送给从节点。
- 从节点先加载RDB文件来构建初始数据集。
- 之后,主节点会将所有新的写命令持续地异步地发送给从节点,从节点执行这些命令以保证数据最终一致性。
优点:
- 数据冗余:实现了数据的热备份,是持久化之外的一种数据冗余方式。
-
读写分离:
- 主节点负责写操作和读操作。
- 从节点可以承担读操作,极大地扩展了读性能。
- 故障恢复基础:为后续实现高可用打下了基础。
缺点:
- 无法实现高可用:主节点故障后,需要手动将从节点提升为主节点,并让应用方更新连接地址,期间服务会中断。
- 写操作无法扩展:所有的写操作都集中在单个主节点上。
- 存储能力无法扩展:每个节点都存储全量数据,无法突破单机内存上限。
6.2、哨兵 (Sentinel)
Redis Sentinel 是一个分布式系统,用于管理 Redis 主从架构,提供高可用性(HA)。它可以理解为是一个“监控员”和“调度员”。
- 工作模式:哨兵本身也是一个独立的进程,通常以奇数个节点(如3个或5个) 组成一个集群运行。
- 核心职责:监控、通知、自动故障转移、配置提供。
如何工作?
- 监控:每个哨兵节点会定期检查所有主节点、从节点以及其他哨兵节点是否正常运行。
- 通知:当被监控的某个 Redis 节点出现问题时,哨兵可以通过 API 向管理员或其他应用程序发送通知。
-
自动故障转移:
- 当一个主节点被判定为“主观下线”(一个哨兵认为它挂了)且“客观下线”(达到法定数量的哨兵都认为它挂了)时。
- 哨兵集群会自动协商选举出一个领导者哨兵来负责故障转移。
- 领导者哨兵会将从节点中选举出一个最优的(例如复制偏移量最新的)并将其提升为新的主节点。
- 让其他从节点改为复制新的主节点。
- 通知客户端应用程序新的主节点地址。
- 配置提供:客户端不再直接连接 Redis 节点,而是连接哨兵集群来查询当前的主节点地址。
优点:
- 高可用:实现了主节点故障的自动切换,极大减少了服务中断时间。
- 完善的监控:提供了完整的监控和报警机制。
缺点:
- 存储和写能力依然无法扩展:和主从复制一样,哨兵模式下的每个节点仍然存储全量数据,写操作也仍在单个主节点上。
6.3、集群 (Cluster)
Redis Cluster 是 Redis 官方提供的数据分片与高可用一体化解决方案。它的核心目标是解决海量数据存储和高并发写的问题。
- 工作模式:去中心化的多主多从架构。一个集群最少需要3个主节点,每个主节点至少对应1个从节点(实现高可用),因此最少需要6个节点。
- 核心职责:数据分片、高可用。
如何工作?
-
数据分片:
- 集群将整个数据集划分为 16384 个哈希槽(Hash Slot)。
- 每个主节点负责处理一部分哈希槽(例如 Node1 负责 0-5000,Node2 负责 5001-10000,Node3 负责 10001-16383)。
- 客户端请求一个 key 时,会通过对 key 计算 CRC16 值再对 16384 取模,决定该 key 属于哪个槽,进而被路由到负责该槽的节点。
-
高可用:
- 每个主节点都可以有多个从节点。
- 主节点提供读写服务,从节点复制主节点数据,并在主节点故障时,自动晋升为主节点(其原理与哨兵类似,集群内置了类似哨兵的故障发现和转移能力)。
- 节点通信:节点之间通过 Gossip 协议进行通信,交换节点元信息、心跳检测等,维持集群状态。
优点:
- 数据与写操作均可扩展:数据分散在不同的主节点上存储,写操作也由不同的主节点分担,突破了单机内存和性能瓶颈。
- 具备高可用性:内置了故障转移功能,无需再额外部署 Sentinel(但原理相似)。
缺点/限制:
- 客户端要求:需要使用支持集群协议的智能客户端,客户端需要缓存槽位配置信息。
-
功能限制:不支持跨多个 key 的事务(除非这些 key 在同一个槽位),不支持
MSET等跨节点命令。Lua 脚本中的 key 也必须位于同一节点。 - 架构和运维更复杂:部署和维护一个多节点的集群比单实例或主从复制要复杂。