一篇文章,总结 Redis 相关的知识

38 阅读22分钟

Redis 基础概念

1. 什么是 Redis?它的主要特点是什么?

Redis (Remote Dictionary Server) 是一个开源的、高性能的键值对(Key-Value)存储系统,常被称为数据结构服务器,因为它支持多种数据结构(字符串、哈希、列表、集合、有序集合等),而不仅仅是简单的字符串。

主要特点:

  • 速度快:数据存储在内存中,读写速度极高,单机每秒可处理十万级别以上的请求。
  • 支持丰富的数据结构:不仅仅是简单的键值存储,还支持字符串 (String)、哈希 (Hash)、列表 (List)、集合 (Set)、有序集合 (Sorted Set)、HyperLogLog、Geospatial 等。
  • 持久化:支持 RDB (Redis Database) 和 AOF (Append Only File) 两种持久化方式,确保数据在服务器重启后不会丢失。
  • 原子性操作:所有操作都是原子性的,要么全部成功,要么全部失败。
  • 支持事务:通过 MULTIEXECDISCARD 等命令实现简单的事务,保证一组命令的原子执行。
  • 发布/订阅:提供 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。计数器(INCRDECR)、缓存(SETGET)、分布式锁(SETNX)。
Hash (哈希)键值对的集合,适用于存储对象。一个 Hash 键可以包含多个 Field-Value 对。存储用户信息(HMSET user:1 name "Alice" age 30)、购物车信息。
List (列表)有序的字符串列表,可以从两端(左/右)推入或弹出元素。消息队列(LPUSH / BRPOP)、最新消息/动态列表(如微博时间线)、排行榜(但通常用 Sorted Set 更方便)。
Set (集合)无序、不重复的字符串集合。标签系统(给文章添加标签)、社交网络(共同关注、共同兴趣)、抽奖系统(SRANDMEMBER)。
Sorted Set (有序集合)有序、不重复的字符串集合,每个成员都会关联一个分数 (score),Redis 根据分数进行排序。排行榜(ZADDZREVRAK)、延时任务队列、带有权重的标签。
HyperLogLog用于对唯一元素进行基数统计(去重计数),空间效率极高,但有一定误差。统计网站 UV(独立访客)、文章阅读量。
Geospatial (地理空间)用于存储地理位置信息(经度、纬度),并根据地理位置进行计算。附近的人、查找指定半径内的地点。

image.png


Redis 持久化机制

4. Redis 的持久化方式有哪些?它们有什么优缺点和使用场景?

Redis 提供两种持久化方式:RDBAOF

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)中。

作用:

  1. 数据冗余与备份:当 Master 实例发生故障时,可以从 Slave 实例中恢复数据,保证数据不丢失。
  2. 读写分离:Master 节点负责所有的写操作,Slave 节点负责读操作,分担 Master 的读压力,提高系统并发能力。
  3. 高可用基础:主从复制是实现 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 的地址。

工作原理:

  1. 多个 Sentinel 进程:通常部署奇数个 Sentinel 进程,它们相互监控,形成一个 Sentinel 集群。

  2. 监控 Redis 实例:每个 Sentinel 都会向 Master 和 Slave 节点发送 PING 命令,判断其是否存活。

  3. 主观下线 (Subjective Down) :如果一个 Sentinel 认为某个 Redis 实例失联,则标记其为“主观下线”。

  4. 客观下线 (Objective Down) :当足够多的 Sentinel (Quorum 数量) 都认为 Master 节点“主观下线”时,Master 被标记为“客观下线”。

  5. 选举领头 Sentinel:当 Master 被客观下线后,Sentinel 之间会进行领导者选举,选出一个领头的 Sentinel 来执行故障转移。

  6. 故障转移

    • 领头的 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 不属于当前节点负责的哈希槽,节点会返回一个 MOVEDASK 重定向指令,指引客户端到正确的节点去执行命令。

作用:

  • 数据分片:将大数据集分散到多个节点,突破单机内存限制。
  • 高可用性:每个 Master 节点可以有多个 Slave 节点。当 Master 节点故障时,其对应的 Slave 节点会自动晋升为新的 Master,继续提供服务。
  • 水平扩展:可以通过增加或移除节点来动态调整集群的容量。

8. Redis Cluster 如何实现高可用?

Redis Cluster 的高可用性主要通过以下机制实现:

  1. 主从复制:每个 Master 节点都可以配置一个或多个 Slave 节点。当 Master 节点挂掉时,其对应的 Slave 节点会自动被选举为新的 Master。
  2. 投票选举:当集群中的 Master 节点下线时,集群中的其他 Master 节点会发起投票,共同选举一个该下线 Master 的 Slave 节点作为新的 Master。
  3. 槽位迁移:当集群节点变动(新增/删除节点)时,Redis Cluster 支持在线的哈希槽迁移,将数据从一个节点平滑地迁移到另一个节点,从而实现扩容或缩容。

Redis 常见应用问题

9. Redis 缓存雪崩、缓存穿透和缓存击穿是什么?如何解决?

这三个问题都是在使用缓存时可能遇到的性能或可用性问题。

缓存雪崩

  • 定义:当大量缓存同时失效(例如,缓存设置了相同的过期时间),或者 Redis 服务器宕机,导致所有请求都直接打到数据库,数据库瞬间压力过大而崩溃,进而导致整个系统崩溃。

  • 原因

    • 缓存层宕机。
    • 大量缓存设置了相同的过期时间。
  • 解决方案

    1. 随机化过期时间:为不同的缓存设置随机的过期时间,避免同时失效。
    2. 高可用架构:搭建 Redis 集群(主从、哨兵、Cluster),保证 Redis 服务的高可用性,防止 Redis 整体宕机。
    3. 熔断/限流:在缓存失效或数据库压力过大时,使用熔断机制,只允许部分请求通过,或直接返回默认值/错误信息,保护数据库。
    4. 多级缓存:引入多级缓存(如本地缓存),在 Redis 失效时,提供一层缓冲。
    5. 服务降级:当系统压力过大时,关闭部分非核心业务,保证核心业务的正常运行。

缓存穿透

  • 定义:查询一个不存在的数据,由于缓存中没有,导致每次请求都直接打到数据库,造成数据库压力。

  • 原因

    • 业务逻辑错误,查询不存在的数据。
    • 恶意攻击,通过大量查询不存在的键来攻击数据库。
  • 解决方案

    1. 缓存空对象:如果查询结果为空,也将这个空结果缓存起来,并设置一个较短的过期时间。下次再查询相同的不存在数据时,直接从缓存返回空,避免访问数据库。
    2. 布隆过滤器 (Bloom Filter) :在查询数据库之前,先通过布隆过滤器判断 Key 是否存在。如果布隆过滤器判断 Key 不存在,则直接返回,避免查询数据库。布隆过滤器存在误判的可能(即存在的数据被误判为不存在),但可以接受。
    3. 参数校验:对非法请求参数进行严格校验,避免恶意请求。

缓存击穿

  • 定义:某个热点数据(高并发访问)的缓存突然失效,大量请求在这一瞬间直接打到数据库,导致数据库压力激增。

  • 原因:热点 Key 在高并发下失效。

  • 解决方案

    1. 设置永不过期:对于一些绝对的热点数据,可以将其缓存设置为永不过期,或者设置一个非常长的过期时间,并通过后台定时任务或消息队列进行更新。

    2. 互斥锁(Mutex Lock) :当发现缓存失效时,只有第一个请求能够去数据库加载数据,并更新缓存,其他请求等待或重试。

      • 可以使用 SETNX 命令实现分布式锁。
    3. 异步更新:设置较短的过期时间,但当缓存即将过期时,异步地去更新缓存,而不是等到真正过期。

    4. 多级缓存:结合本地缓存,降低对 Redis 的依赖。

10. Redis 的过期键删除策略有哪些?

Redis 主要采用三种策略来删除过期键:

  1. 惰性删除 (Lazy Deletion)

    • 原理:当客户端尝试访问某个键时,Redis 会检查该键是否过期。如果过期,则立即删除并返回 nil
    • 优点:对 CPU 友好,只在访问时才进行删除操作。
    • 缺点:如果一个过期键长时间未被访问,它会一直占用内存,造成内存浪费。
  2. 定期删除 (Active Deletion)

    • 原理:Redis 会周期性地(默认每秒 10 次)随机抽取一些设置了过期时间的键,检查它们是否过期。如果过期,则删除。

    • 优点:在 CPU 和内存之间做了一个折中,既能及时清理一部分过期键,又不会过度消耗 CPU。

    • 缺点

      • 难以精确控制,无法保证所有过期键都能被及时删除。
      • 如果过期键的数量非常大,每次随机抽取和检查仍然会消耗一定的 CPU 资源。
  3. 内存淘汰策略 (Eviction Policy)

    • 原理:当 Redis 内存达到设定的最大内存限制 (maxmemory) 时,会根据配置的淘汰策略强制删除一部分键,直到内存使用量低于限制。
    • 优点:保证 Redis 不会因为内存耗尽而崩溃。
    • 缺点:可能会删除非过期键,或者删除不应该删除的键,需要谨慎配置。

常用的内存淘汰策略:

  • noeviction (默认):当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:从所有键中选择最近最少使用的键进行淘汰。
  • volatile-lru:从所有设置了过期时间的键中选择最近最少使用的键进行淘汰。
  • allkeys-random:从所有键中随机淘汰。
  • volatile-random:从所有设置了过期时间的键中随机淘汰。
  • allkeys-ttl:从所有键中选择即将过期的键进行淘汰(更接近过期时间)。
  • volatile-ttl:从所有设置了过期时间的键中选择即将过期的键进行淘汰。

11. Redis 如何实现分布式锁?

使用 Redis 实现分布式锁是常见的应用场景,主要利用 Redis 的原子性操作过期时间特性。

实现方式一:SETNX + EXPIRE (已废弃,不推荐)

  1. SETNX key value:当 key 不存在时,设置 key 的值为 value,返回 1;否则不操作,返回 0。

    • 如果返回 1,表示获取锁成功。
    • 如果返回 0,表示获取锁失败。
  2. EXPIRE key seconds:为 key 设置过期时间,防止死锁。

问题SETNXEXPIRE非原子性的。如果在 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 算法原理:

  1. 假设有 N 个独立的 Redis Master 节点(通常 N 为奇数,例如 5 个)。
  2. 客户端尝试在 N 个节点上串行地获取锁。
  3. 只有当客户端在 N/2 + 1 个节点上成功获取锁,并且获取锁的总时间小于锁的有效时间时,才认为获取锁成功。
  4. 释放锁时,客户端向所有 N 个节点发送解锁请求。

Redlock 算法虽然提高了锁的可靠性,但也增加了复杂性,且在实际应用中仍存在一些争议和限制。对于大多数分布式锁需求,单 Redis 实例的 SETNX 原子操作配合 Lua 脚本通常已经足够。


Redis 事务

12. Redis 事务的特点和限制是什么?

Redis 事务通过 MULTIEXECDISCARD 等命令实现,它提供了一种将多个命令打包执行的机制。

特点:

  1. 原子性:事务中的所有命令会被作为一个原子操作执行,要么全部执行,要么全部不执行。
  2. 独立隔离:事务在执行过程中,不会被其他客户端的命令打断,所有命令会按顺序执行。
  3. 不具备回滚能力:与传统关系型数据库事务不同,Redis 事务不支持回滚。如果在事务执行过程中,有某个命令执行失败(例如类型错误),其他命令仍然会继续执行。Redis 事务只保证命令的原子提交,不保证执行的语义正确性
  4. 命令入队MULTI 之后,所有命令会被放入一个队列,直到 EXEC 命令执行时才会被原子性地提交到 Redis 服务器。

主要限制:

  • 不支持回滚:这是 Redis 事务最大的特点和限制。一旦入队命令有错误(语法错误在入队时就能发现,执行错误只能在 EXEC 时发现),不影响其他命令的执行。
  • 不保证命令的语义正确性:如果事务中的命令在执行时发生了运行时错误(如对字符串类型执行列表操作),Redis 不会回滚整个事务,而是继续执行后续命令。
  • 乐观锁:Redis 事务通常结合 WATCH 命令实现乐观锁,用于监控键的变化。如果在 EXECWATCH 的键被修改,事务会被取消。

示例:

MULTI
SET name "Alice"
INCR age  // 假设 age 不存在,会被自动初始化为 0,然后递增到 1
LPUSH mylist "item1" "item2"
GET name
EXEC

Redis 内存管理

13. Redis 如何进行内存管理?

Redis 的内存管理主要涉及以下几个方面:

  1. 数据存储:Redis 的所有数据都存储在内存中。键和值都会占用内存空间。
  2. 内存分配器:Redis 默认使用 jemalloc 作为内存分配器(Linux),或者 tcmalloc (Google Performance Tools)。这些内存分配器比系统自带的 libc malloc 更高效,能减少内存碎片。
  3. 内存用量统计INFO memory 命令可以查看 Redis 的内存使用情况,如 used_memory (Redis 实际使用的内存量)、used_memory_rss (操作系统分配给 Redis 的内存量) 等。
  4. maxmemory 配置:通过 maxmemory 参数可以限制 Redis 实例可以使用的最大内存量。
  5. 内存淘汰策略:当内存达到 maxmemory 限制时,Redis 会根据配置的淘汰策略(maxmemory-policy)来删除键,以释放内存。详情见问题 10。
  6. RDB/AOF 内存占用:在进行 RDB 持久化或 AOF 重写时,Redis 会 fork 子进程。子进程会复制父进程的内存页表,从而实现写时复制 (Copy-On-Write, COW) 。这意味着只有当父进程修改某个内存页时,该页才会被真正复制一份,避免了在 fork 时立即复制所有内存,节省了内存资源。但仍然可能在写入量大时,导致父子进程共享内存的复制,从而使得实际内存使用量翻倍。

其他常见问题

14. Redis 和 Memcached 有什么区别?

特性RedisMemcached
数据结构支持丰富的数据结构(String, Hash, List, Set, Sorted Set, Geospatial, HyperLogLog)只支持简单的键值对(String)
持久化支持 RDB 和 AOF 两种持久化方式,数据可恢复不支持持久化,数据存储在内存中,服务重启后丢失
主从复制支持主从复制,可用于读写分离和高可用不支持主从复制,需通过客户端或第三方工具实现
高可用支持 Sentinel (自动故障转移) 和 Cluster (分片+高可用)不支持原生高可用,通常通过客户端一致性哈希或代理实现
内存管理内部实现更优化,支持不同的内存分配器简单的 LRU 淘汰机制,内存管理相对简单
多核利用单线程模型,但通过 I/O 多路复用高效处理并发请求多线程模型,但锁竞争可能带来性能开销
使用场景复杂数据结构、计数器、排行榜、消息队列、缓存、分布式锁等简单的纯缓存场景,对数据结构和持久化无要求

导出到 Google 表格

15. 如何保证 Redis 的数据一致性?

数据一致性是分布式系统中的一个挑战。在 Redis 中,主要通过以下方式保证:

  1. 强一致性(最终一致性)

    • 主从复制:通过主从复制实现数据的异步同步。虽然 Master-Slave 是异步复制,但可以最大限度保证最终一致性。对于要求不严格的场景,这是可以接受的。
    • AOF 持久化:开启 appendfsync always 模式(高安全性但影响性能)或 everysec 模式,将写操作实时或每秒刷新到磁盘,减少数据丢失。
    • Sentinel/Cluster 故障转移:在主节点故障时,Sentinel 或 Cluster 会自动进行故障转移,选择一个拥有最新数据的 Slave 节点作为新的 Master。
  2. 读写分离引发的一致性问题

    • 延时读取问题:Master 写入数据后,由于主从复制的延迟,立即从 Slave 读取可能读到旧数据。

    • 解决方案

      • 强制读 Master:对一致性要求高的操作,强制从 Master 节点读取。

      • 读写分离延迟检测:通过监控 Master 和 Slave 的复制偏移量,当延迟过大时,将读请求路由到 Master。

      • 缓存更新策略

        • Cache Aside Pattern (旁路缓存模式)

          • 读操作:先查缓存,命中则返回;未命中则查数据库,将结果写入缓存,再返回。
          • 写操作:先更新数据库,再删除缓存(或更新缓存)。先删除缓存再更新数据库可能会导致短暂的不一致(数据库已更新,但旧的缓存被读到,然后写入缓存)。所以一般建议先更新数据库,再删除缓存。
        • Read/Write Through Pattern (读/写穿透模式) :通常由缓存框架(如 Guava Cache)实现,应用程序只和缓存交互,缓存负责与数据库的同步。

        • Write Back Pattern (写回模式) :写操作只写入缓存,由缓存异步写入数据库。性能最好,但数据丢失风险高,一般不用于关键业务。

  3. 分布式事务:对于涉及多个数据源(如 Redis 和关系型数据库)的复杂事务,Redis 本身不提供分布式事务解决方案。通常需要借助外部分布式事务框架(如 Seata)或采用最终一致性的柔性事务方案(如消息队列、TCC、Saga)。