Redis 基础概念
1. 什么是 Redis?它的主要特点是什么?
Redis (Remote Dictionary Server) 是一个开源的、高性能的键值对(Key-Value)存储系统,常被称为数据结构服务器,因为它支持多种数据结构(字符串、哈希、列表、集合、有序集合等),而不仅仅是简单的字符串。
主要特点:
- 速度快:数据存储在内存中,读写速度极高,单机每秒可处理十万级别以上的请求。
- 支持丰富的数据结构:不仅仅是简单的键值存储,还支持字符串 (String)、哈希 (Hash)、列表 (List)、集合 (Set)、有序集合 (Sorted Set)、HyperLogLog、Geospatial 等。
- 持久化:支持 RDB (Redis Database) 和 AOF (Append Only File) 两种持久化方式,确保数据在服务器重启后不会丢失。
- 原子性操作:所有操作都是原子性的,要么全部成功,要么全部失败。
- 支持事务:通过
MULTI
、EXEC
、DISCARD
等命令实现简单的事务,保证一组命令的原子执行。 - 发布/订阅:提供 Pub/Sub 机制,用于消息通知。
- 高可用和分布式:支持主从复制 (Master-Slave Replication)、Sentinel (哨兵) 和 Cluster (集群) 模式,实现高可用和水平扩展。
- 简单易用:命令简洁,学习曲线平缓。
2. Redis 为什么这么快?
Redis 速度快的原因主要有以下几点:
- 基于内存操作:所有数据都存储在内存中,避免了磁盘 I/O 的性能瓶颈。
- 单线程模型:Redis 使用单线程处理所有客户端请求,避免了多线程带来的上下文切换和锁竞争开销。这意味着所有的操作都是串行执行的,保证了原子性。
- 高效的数据结构:Redis 底层对各种数据结构进行了高度优化,例如跳表、压缩列表等,使得操作效率极高。
- I/O 多路复用:Redis 使用 I/O 多路复用技术(如 epoll、select、kqueue),可以在单线程中监听多个文件描述符(客户端连接),实现并发处理多个客户端请求。
- C 语言实现:C 语言的执行效率高,且对内存操作有更精细的控制。
Redis 数据结构与使用场景
3. Redis 支持哪些数据结构?请举例说明它们的应用场景。
数据结构 | 描述 | 应用场景 |
---|---|---|
String (字符串) | 最基本的数据类型,可以存储文本、数字或二进制数据,最大 512MB。 | 计数器(INCR 、DECR )、缓存(SET 、GET )、分布式锁(SETNX )。 |
Hash (哈希) | 键值对的集合,适用于存储对象。一个 Hash 键可以包含多个 Field-Value 对。 | 存储用户信息(HMSET user:1 name "Alice" age 30 )、购物车信息。 |
List (列表) | 有序的字符串列表,可以从两端(左/右)推入或弹出元素。 | 消息队列(LPUSH / BRPOP )、最新消息/动态列表(如微博时间线)、排行榜(但通常用 Sorted Set 更方便)。 |
Set (集合) | 无序、不重复的字符串集合。 | 标签系统(给文章添加标签)、社交网络(共同关注、共同兴趣)、抽奖系统(SRANDMEMBER )。 |
Sorted Set (有序集合) | 有序、不重复的字符串集合,每个成员都会关联一个分数 (score),Redis 根据分数进行排序。 | 排行榜(ZADD 、ZREVRAK )、延时任务队列、带有权重的标签。 |
HyperLogLog | 用于对唯一元素进行基数统计(去重计数),空间效率极高,但有一定误差。 | 统计网站 UV(独立访客)、文章阅读量。 |
Geospatial (地理空间) | 用于存储地理位置信息(经度、纬度),并根据地理位置进行计算。 | 附近的人、查找指定半径内的地点。 |
Redis 持久化机制
4. Redis 的持久化方式有哪些?它们有什么优缺点和使用场景?
Redis 提供两种持久化方式:RDB 和 AOF。
RDB (Redis Database)
-
原理:在指定时间间隔内将内存中的数据集快照写入磁盘,生成一个
.rdb
文件。当 Redis 重启时,会加载.rdb
文件恢复数据。 -
优点:
- RDB 文件是一个紧凑的二进制文件,非常适合用于备份和灾难恢复。
- 恢复速度快,因为是直接加载快照文件。
- Redis 父进程在保存 RDB 文件时,唯一需要做的就是
fork
出一个子进程,由子进程完成所有持久化工作,父进程继续处理客户端请求,性能影响小。
-
缺点:
- 数据实时性低:RDB 是周期性保存的,如果在两次保存之间 Redis 发生宕机,会丢失一部分数据。
fork
子进程会消耗一定的系统资源(主要是内存),当数据量很大时,fork
过程可能会有短暂的阻塞。
-
使用场景:对数据完整性要求不高,或主要用于数据备份和快速恢复的场景。
AOF (Append Only File)
-
原理:以日志的形式记录 Redis 执行的每个写操作。当 Redis 重启时,会重新执行 AOF 文件中的所有命令来恢复数据。
-
优点:
- 数据完整性高:可以通过配置不同的
fsync
策略来保证数据丢失的最少。always
模式下,几乎不丢失数据。 - AOF 文件是纯文本格式,可读性强,方便进行故障排查。
- AOF 重写机制(
BGREWRITEAOF
)可以压缩 AOF 文件,去除冗余命令。
- 数据完整性高:可以通过配置不同的
-
缺点:
- AOF 文件通常比 RDB 文件大,因为它记录了所有的写命令。
- 恢复速度慢,需要重新执行所有命令。
fsync
策略配置为always
时,每次写操作都会同步到磁盘,会带来一定的 I/O 开销。
-
使用场景:对数据完整性要求高,或需要查看操作日志的场景。
如何选择?
- 最佳实践:通常建议同时开启 RDB 和 AOF。RDB 用于定期备份,AOF 用于保证数据的实时性和完整性。当数据恢复时,会优先使用 AOF 文件。
- 如果只追求性能且能接受少量数据丢失,可以只使用 RDB。
- 如果对数据完整性要求极高,且能接受一定的性能损失和更大的文件,可以只使用 AOF。
Redis 高可用与分布式
5. Redis 主从复制 (Master-Slave Replication) 是什么?它有什么作用?
主从复制是 Redis 提供的一种高可用机制,通过将一个 Redis 实例(Master)的数据复制到其他 Redis 实例(Slave)中。
作用:
- 数据冗余与备份:当 Master 实例发生故障时,可以从 Slave 实例中恢复数据,保证数据不丢失。
- 读写分离:Master 节点负责所有的写操作,Slave 节点负责读操作,分担 Master 的读压力,提高系统并发能力。
- 高可用基础:主从复制是实现 Redis Sentinel 和 Redis Cluster 的基础。
6. Redis Sentinel (哨兵) 是什么?它的作用和工作原理是什么?
Redis Sentinel 是 Redis 提供的一个高可用解决方案,它是一个监控系统,用于自动化管理 Redis 主从复制集群。
作用:
- 监控 (Monitoring) :持续检查 Master 和 Slave 实例是否正常运行。
- 通知 (Notification) :当被监控的 Redis 实例出现问题时,通过 API 向管理员或其他应用程序发送通知。
- 自动故障转移 (Automatic Failover) :当 Master 实例发生故障时,Sentinel 会自动将一个健康的 Slave 提升为新的 Master,并通知其他 Slave 切换到新的 Master。
- 配置提供者 (Configuration Provider) :客户端可以通过 Sentinel 获取当前 Master 的地址。
工作原理:
-
多个 Sentinel 进程:通常部署奇数个 Sentinel 进程,它们相互监控,形成一个 Sentinel 集群。
-
监控 Redis 实例:每个 Sentinel 都会向 Master 和 Slave 节点发送 PING 命令,判断其是否存活。
-
主观下线 (Subjective Down) :如果一个 Sentinel 认为某个 Redis 实例失联,则标记其为“主观下线”。
-
客观下线 (Objective Down) :当足够多的 Sentinel (Quorum 数量) 都认为 Master 节点“主观下线”时,Master 被标记为“客观下线”。
-
选举领头 Sentinel:当 Master 被客观下线后,Sentinel 之间会进行领导者选举,选出一个领头的 Sentinel 来执行故障转移。
-
故障转移:
- 领头的 Sentinel 从所有健康的 Slave 中,选择一个最合适的 Slave(通常是复制偏移量最大、优先级最高的)提升为新的 Master。
- 向其他的 Slave 发送
REPLICAOF
命令,让它们去复制新的 Master。 - 更新客户端的 Master 地址信息。
7. Redis Cluster (集群) 是什么?它的分片原理是什么?
Redis Cluster 是 Redis 官方提供的分布式解决方案,旨在提供高可用性和可扩展性。它通过分片 (Sharding) 的方式将数据分布到多个 Redis 节点上,突破了单机 Redis 的内存和并发瓶颈。
分片原理 (哈希槽):
Redis Cluster 采用哈希槽 (Hash Slot) 的方式进行数据分片。
-
Redis Cluster 共有 16384 个哈希槽 (0 到 16383)。
-
每个键在存储时,都会根据其 Key 计算出一个 CRC16 值,然后将这个值对 16384 取模,得到一个哈希槽编号。
hash_slot = CRC16(key) % 16384
-
每个 Redis Cluster 节点负责一部分哈希槽。例如,一个 3 节点的集群,节点 A 可能负责 0-5460,节点 B 负责 5461-10922,节点 C 负责 10923-16383。
-
当客户端向任意一个 Redis 节点发送命令时,如果该 Key 不属于当前节点负责的哈希槽,节点会返回一个
MOVED
或ASK
重定向指令,指引客户端到正确的节点去执行命令。
作用:
- 数据分片:将大数据集分散到多个节点,突破单机内存限制。
- 高可用性:每个 Master 节点可以有多个 Slave 节点。当 Master 节点故障时,其对应的 Slave 节点会自动晋升为新的 Master,继续提供服务。
- 水平扩展:可以通过增加或移除节点来动态调整集群的容量。
8. Redis Cluster 如何实现高可用?
Redis Cluster 的高可用性主要通过以下机制实现:
- 主从复制:每个 Master 节点都可以配置一个或多个 Slave 节点。当 Master 节点挂掉时,其对应的 Slave 节点会自动被选举为新的 Master。
- 投票选举:当集群中的 Master 节点下线时,集群中的其他 Master 节点会发起投票,共同选举一个该下线 Master 的 Slave 节点作为新的 Master。
- 槽位迁移:当集群节点变动(新增/删除节点)时,Redis Cluster 支持在线的哈希槽迁移,将数据从一个节点平滑地迁移到另一个节点,从而实现扩容或缩容。
Redis 常见应用问题
9. Redis 缓存雪崩、缓存穿透和缓存击穿是什么?如何解决?
这三个问题都是在使用缓存时可能遇到的性能或可用性问题。
缓存雪崩
-
定义:当大量缓存同时失效(例如,缓存设置了相同的过期时间),或者 Redis 服务器宕机,导致所有请求都直接打到数据库,数据库瞬间压力过大而崩溃,进而导致整个系统崩溃。
-
原因:
- 缓存层宕机。
- 大量缓存设置了相同的过期时间。
-
解决方案:
- 随机化过期时间:为不同的缓存设置随机的过期时间,避免同时失效。
- 高可用架构:搭建 Redis 集群(主从、哨兵、Cluster),保证 Redis 服务的高可用性,防止 Redis 整体宕机。
- 熔断/限流:在缓存失效或数据库压力过大时,使用熔断机制,只允许部分请求通过,或直接返回默认值/错误信息,保护数据库。
- 多级缓存:引入多级缓存(如本地缓存),在 Redis 失效时,提供一层缓冲。
- 服务降级:当系统压力过大时,关闭部分非核心业务,保证核心业务的正常运行。
缓存穿透
-
定义:查询一个不存在的数据,由于缓存中没有,导致每次请求都直接打到数据库,造成数据库压力。
-
原因:
- 业务逻辑错误,查询不存在的数据。
- 恶意攻击,通过大量查询不存在的键来攻击数据库。
-
解决方案:
- 缓存空对象:如果查询结果为空,也将这个空结果缓存起来,并设置一个较短的过期时间。下次再查询相同的不存在数据时,直接从缓存返回空,避免访问数据库。
- 布隆过滤器 (Bloom Filter) :在查询数据库之前,先通过布隆过滤器判断 Key 是否存在。如果布隆过滤器判断 Key 不存在,则直接返回,避免查询数据库。布隆过滤器存在误判的可能(即存在的数据被误判为不存在),但可以接受。
- 参数校验:对非法请求参数进行严格校验,避免恶意请求。
缓存击穿
-
定义:某个热点数据(高并发访问)的缓存突然失效,大量请求在这一瞬间直接打到数据库,导致数据库压力激增。
-
原因:热点 Key 在高并发下失效。
-
解决方案:
-
设置永不过期:对于一些绝对的热点数据,可以将其缓存设置为永不过期,或者设置一个非常长的过期时间,并通过后台定时任务或消息队列进行更新。
-
互斥锁(Mutex Lock) :当发现缓存失效时,只有第一个请求能够去数据库加载数据,并更新缓存,其他请求等待或重试。
- 可以使用
SETNX
命令实现分布式锁。
- 可以使用
-
异步更新:设置较短的过期时间,但当缓存即将过期时,异步地去更新缓存,而不是等到真正过期。
-
多级缓存:结合本地缓存,降低对 Redis 的依赖。
-
10. Redis 的过期键删除策略有哪些?
Redis 主要采用三种策略来删除过期键:
-
惰性删除 (Lazy Deletion) :
- 原理:当客户端尝试访问某个键时,Redis 会检查该键是否过期。如果过期,则立即删除并返回
nil
。 - 优点:对 CPU 友好,只在访问时才进行删除操作。
- 缺点:如果一个过期键长时间未被访问,它会一直占用内存,造成内存浪费。
- 原理:当客户端尝试访问某个键时,Redis 会检查该键是否过期。如果过期,则立即删除并返回
-
定期删除 (Active Deletion) :
-
原理:Redis 会周期性地(默认每秒 10 次)随机抽取一些设置了过期时间的键,检查它们是否过期。如果过期,则删除。
-
优点:在 CPU 和内存之间做了一个折中,既能及时清理一部分过期键,又不会过度消耗 CPU。
-
缺点:
- 难以精确控制,无法保证所有过期键都能被及时删除。
- 如果过期键的数量非常大,每次随机抽取和检查仍然会消耗一定的 CPU 资源。
-
-
内存淘汰策略 (Eviction Policy) :
- 原理:当 Redis 内存达到设定的最大内存限制 (maxmemory) 时,会根据配置的淘汰策略强制删除一部分键,直到内存使用量低于限制。
- 优点:保证 Redis 不会因为内存耗尽而崩溃。
- 缺点:可能会删除非过期键,或者删除不应该删除的键,需要谨慎配置。
常用的内存淘汰策略:
noeviction
(默认):当内存不足以容纳新写入数据时,新写入操作会报错。allkeys-lru
:从所有键中选择最近最少使用的键进行淘汰。volatile-lru
:从所有设置了过期时间的键中选择最近最少使用的键进行淘汰。allkeys-random
:从所有键中随机淘汰。volatile-random
:从所有设置了过期时间的键中随机淘汰。allkeys-ttl
:从所有键中选择即将过期的键进行淘汰(更接近过期时间)。volatile-ttl
:从所有设置了过期时间的键中选择即将过期的键进行淘汰。
11. Redis 如何实现分布式锁?
使用 Redis 实现分布式锁是常见的应用场景,主要利用 Redis 的原子性操作和过期时间特性。
实现方式一:SETNX + EXPIRE
(已废弃,不推荐)
-
SETNX key value
:当key
不存在时,设置key
的值为value
,返回 1;否则不操作,返回 0。- 如果返回 1,表示获取锁成功。
- 如果返回 0,表示获取锁失败。
-
EXPIRE key seconds
:为key
设置过期时间,防止死锁。
问题:SETNX
和 EXPIRE
是非原子性的。如果在 SETNX
成功后,但在 EXPIRE
之前 Redis 宕机,锁将永远不会释放,造成死锁。
实现方式二:SET key value EX PX NX
(推荐)
Redis 2.6.12 版本后,SET
命令增加了多个选项,可以实现原子性的设置键值和过期时间。
-
SET key value [EX seconds] [PX milliseconds] [NX|XX]
NX
:只在键不存在时设置键。EX seconds
:设置键的过期时间,单位为秒。PX milliseconds
:设置键的过期时间,单位为毫秒。
示例: SET lock_key unique_id EX 10 NX
lock_key
:锁的名称。unique_id
:客户端请求的唯一标识符(例如,UUID),用于安全释放锁。EX 10
:锁的过期时间为 10 秒。NX
:只有当lock_key
不存在时才设置。
解锁操作 (Lua 脚本):
为了避免误删其他客户端的锁,解锁时需要判断锁的 value
是否与自己设置的 value
相同,这个判断和删除的操作必须是原子性的。这可以通过 Lua 脚本实现:
Lua
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
解释:
KEYS[1]
是锁的key
(lock_key
)。ARGV[1]
是客户端的唯一标识 (unique_id
)。- 先
GET
锁的值,判断是否是当前客户端设置的。 - 如果是,则
DEL
锁,释放成功。 - 否则,不操作,返回 0。
进阶考虑:Redlock 算法 (多节点分布式锁)
当 Redis 集群部署在多个独立节点时,即使某个 Master 节点故障并发生了主从切换,也可能导致锁的短暂失效。为了解决这个问题,Redis 的作者 Antirez 提出了 Redlock 算法。
Redlock 算法原理:
- 假设有 N 个独立的 Redis Master 节点(通常 N 为奇数,例如 5 个)。
- 客户端尝试在 N 个节点上串行地获取锁。
- 只有当客户端在 N/2 + 1 个节点上成功获取锁,并且获取锁的总时间小于锁的有效时间时,才认为获取锁成功。
- 释放锁时,客户端向所有 N 个节点发送解锁请求。
Redlock 算法虽然提高了锁的可靠性,但也增加了复杂性,且在实际应用中仍存在一些争议和限制。对于大多数分布式锁需求,单 Redis 实例的 SETNX
原子操作配合 Lua 脚本通常已经足够。
Redis 事务
12. Redis 事务的特点和限制是什么?
Redis 事务通过 MULTI
、EXEC
、DISCARD
等命令实现,它提供了一种将多个命令打包执行的机制。
特点:
- 原子性:事务中的所有命令会被作为一个原子操作执行,要么全部执行,要么全部不执行。
- 独立隔离:事务在执行过程中,不会被其他客户端的命令打断,所有命令会按顺序执行。
- 不具备回滚能力:与传统关系型数据库事务不同,Redis 事务不支持回滚。如果在事务执行过程中,有某个命令执行失败(例如类型错误),其他命令仍然会继续执行。Redis 事务只保证命令的原子提交,不保证执行的语义正确性。
- 命令入队:
MULTI
之后,所有命令会被放入一个队列,直到EXEC
命令执行时才会被原子性地提交到 Redis 服务器。
主要限制:
- 不支持回滚:这是 Redis 事务最大的特点和限制。一旦入队命令有错误(语法错误在入队时就能发现,执行错误只能在
EXEC
时发现),不影响其他命令的执行。 - 不保证命令的语义正确性:如果事务中的命令在执行时发生了运行时错误(如对字符串类型执行列表操作),Redis 不会回滚整个事务,而是继续执行后续命令。
- 乐观锁:Redis 事务通常结合
WATCH
命令实现乐观锁,用于监控键的变化。如果在EXEC
前WATCH
的键被修改,事务会被取消。
示例:
MULTI
SET name "Alice"
INCR age // 假设 age 不存在,会被自动初始化为 0,然后递增到 1
LPUSH mylist "item1" "item2"
GET name
EXEC
Redis 内存管理
13. Redis 如何进行内存管理?
Redis 的内存管理主要涉及以下几个方面:
- 数据存储:Redis 的所有数据都存储在内存中。键和值都会占用内存空间。
- 内存分配器:Redis 默认使用
jemalloc
作为内存分配器(Linux),或者tcmalloc
(Google Performance Tools)。这些内存分配器比系统自带的libc malloc
更高效,能减少内存碎片。 - 内存用量统计:
INFO memory
命令可以查看 Redis 的内存使用情况,如used_memory
(Redis 实际使用的内存量)、used_memory_rss
(操作系统分配给 Redis 的内存量) 等。 maxmemory
配置:通过maxmemory
参数可以限制 Redis 实例可以使用的最大内存量。- 内存淘汰策略:当内存达到
maxmemory
限制时,Redis 会根据配置的淘汰策略(maxmemory-policy
)来删除键,以释放内存。详情见问题 10。 - RDB/AOF 内存占用:在进行 RDB 持久化或 AOF 重写时,Redis 会
fork
子进程。子进程会复制父进程的内存页表,从而实现写时复制 (Copy-On-Write, COW) 。这意味着只有当父进程修改某个内存页时,该页才会被真正复制一份,避免了在fork
时立即复制所有内存,节省了内存资源。但仍然可能在写入量大时,导致父子进程共享内存的复制,从而使得实际内存使用量翻倍。
其他常见问题
14. Redis 和 Memcached 有什么区别?
特性 | Redis | Memcached |
---|---|---|
数据结构 | 支持丰富的数据结构(String, Hash, List, Set, Sorted Set, Geospatial, HyperLogLog) | 只支持简单的键值对(String) |
持久化 | 支持 RDB 和 AOF 两种持久化方式,数据可恢复 | 不支持持久化,数据存储在内存中,服务重启后丢失 |
主从复制 | 支持主从复制,可用于读写分离和高可用 | 不支持主从复制,需通过客户端或第三方工具实现 |
高可用 | 支持 Sentinel (自动故障转移) 和 Cluster (分片+高可用) | 不支持原生高可用,通常通过客户端一致性哈希或代理实现 |
内存管理 | 内部实现更优化,支持不同的内存分配器 | 简单的 LRU 淘汰机制,内存管理相对简单 |
多核利用 | 单线程模型,但通过 I/O 多路复用高效处理并发请求 | 多线程模型,但锁竞争可能带来性能开销 |
使用场景 | 复杂数据结构、计数器、排行榜、消息队列、缓存、分布式锁等 | 简单的纯缓存场景,对数据结构和持久化无要求 |
导出到 Google 表格
15. 如何保证 Redis 的数据一致性?
数据一致性是分布式系统中的一个挑战。在 Redis 中,主要通过以下方式保证:
-
强一致性(最终一致性) :
- 主从复制:通过主从复制实现数据的异步同步。虽然 Master-Slave 是异步复制,但可以最大限度保证最终一致性。对于要求不严格的场景,这是可以接受的。
- AOF 持久化:开启
appendfsync always
模式(高安全性但影响性能)或everysec
模式,将写操作实时或每秒刷新到磁盘,减少数据丢失。 - Sentinel/Cluster 故障转移:在主节点故障时,Sentinel 或 Cluster 会自动进行故障转移,选择一个拥有最新数据的 Slave 节点作为新的 Master。
-
读写分离引发的一致性问题:
-
延时读取问题:Master 写入数据后,由于主从复制的延迟,立即从 Slave 读取可能读到旧数据。
-
解决方案:
-
强制读 Master:对一致性要求高的操作,强制从 Master 节点读取。
-
读写分离延迟检测:通过监控 Master 和 Slave 的复制偏移量,当延迟过大时,将读请求路由到 Master。
-
缓存更新策略:
-
Cache Aside Pattern (旁路缓存模式) :
- 读操作:先查缓存,命中则返回;未命中则查数据库,将结果写入缓存,再返回。
- 写操作:先更新数据库,再删除缓存(或更新缓存)。先删除缓存再更新数据库可能会导致短暂的不一致(数据库已更新,但旧的缓存被读到,然后写入缓存)。所以一般建议先更新数据库,再删除缓存。
-
Read/Write Through Pattern (读/写穿透模式) :通常由缓存框架(如 Guava Cache)实现,应用程序只和缓存交互,缓存负责与数据库的同步。
-
Write Back Pattern (写回模式) :写操作只写入缓存,由缓存异步写入数据库。性能最好,但数据丢失风险高,一般不用于关键业务。
-
-
-
-
分布式事务:对于涉及多个数据源(如 Redis 和关系型数据库)的复杂事务,Redis 本身不提供分布式事务解决方案。通常需要借助外部分布式事务框架(如 Seata)或采用最终一致性的柔性事务方案(如消息队列、TCC、Saga)。