找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4668

积分

0

好友

641

主题
发表于 3 天前 | 查看: 16| 回复: 0

两名工程师在讨论Redis技术方案

谈到高性能缓存与内存数据库,Redis 无疑是绕不开的核心组件。无论是应对高并发读写的缓存场景,还是作为分布式锁、消息队列的解决方案,Redis 都扮演着至关重要的角色。对于开发者而言,深入理解其原理是提升系统设计能力和通过技术面试的关键。本文将带你系统梳理 Redis 的核心知识点,内容源于云栈社区 的技术讨论与实践总结。

什么是 Redis?

  • 定义:Redis(Remote Dictionary Server)是一个使用 C语言 编写的、开源(BSD许可)的高性能 非关系型(NoSQL) 键值对数据库。
  • 核心能力:它可以存储键与五种不同类型值之间的映射。
  • 特性与用途:Redis 支持数据持久化、主从复制、分片等高级特性,读写速度极快,每秒可处理超过 10万次 读写操作,是已知性能最快的 Key-Value 数据库。因此,它被广泛应用于缓存、分布式锁等场景,并支持事务、LUA脚本、LRU驱动事件及多种集群方案。

Redis 为什么那么快?

  1. 完全基于内存:绝大部分请求是纯粹的内存操作,避免了磁盘I/O的瓶颈。
  2. 数据结构简单:其数据结构是专门为各种场景设计的,对数据的操作也简单高效。
  3. 采用单线程模型:避免了不必要的上下文切换和竞争条件,也无需考虑多线程环境下的各种锁问题,如死锁导致的性能消耗。
  4. 使用多路 I/O 复用模型:基于非阻塞 I/O,可以高效处理大量并发连接。

Redis 的优缺点

优点

  • 读写速度快:数据存在于内存中,类似于 HashMap 的访问效率。
  • 支持丰富数据类型:包括 String,List,Hash,Set,Sorted Set(Zset)。
  • 支持事务:提供基本的一致性和隔离性保证,结合 RDB 和 AOF 也能实现一定的持久性。
  • 支持主从复制:主机自动同步数据到从机,便于实现读写分离。

缺点

  • 缓存和数据库双写一致性问题。
  • 缓存雪崩、穿透、击穿问题。
  • 在线扩容较复杂,集群容量达到上限时扩容流程繁琐。
  • 主机宕机前可能部分数据未及时同步到从机,切换后可能引发数据不一致,影响系统可用性。
  • 缓存的并发竞争问题。

Redis 与 Memcached 的区别

  • 数据类型
    • Memcached 仅支持字符串类型。
    • Redis 支持五种不同的数据类型,灵活性更高。
  • 数据持久化
    • Memcached 不支持持久化。
    • Redis 支持两种持久化策略:RDB 快照和 AOF 日志。
  • 分布式
    • Memcached 本身不支持分布式,需客户端通过一致性哈希实现,存储和查询时都需计算数据所在节点。
    • Redis Cluster 原生支持分布式。
  • 内存管理机制
    • 在 Redis 中,并非所有数据都常驻内存,可以将不常用的 value 交换到磁盘。
    • Memcached 的数据始终在内存中,它通过预分配特定长度的内存块来存储数据,以解决内存碎片,但可能导致内存利用率不高(例如128 bytes块只存100 bytes数据,造成浪费)。

Redis 有哪些数据类型?

String(字符串)

  • 简介:二进制安全的字符串。
  • 可存储的值:字符串、整数、浮点数,甚至是 jpg 图片或序列化的对象。
  • 操作:可对整个字符串或其一部分执行操作;对整数和浮点数执行自增/自减。
  • 应用场景:简单的键值对缓存。
  • 实际使用
    > set test cbuc
    OK
    ----------------------------
    > get test
    "cbuc"
    ----------------------------
    > del test
    (integer) 1
    ----------------------------
    >get test
    (nil)

List(列表)

  • 简介:基于双向链表实现。
  • 可存储的值:列表。
  • 操作:从两端压入或弹出元素;对单个或多个元素进行修改;保留指定范围内的元素。
  • 应用场景:最新消息排行;消息队列。
  • 实际使用
    > rpush test c1
    (integer) 1
    > rpush test c2
    (integer) 2
    > rpush test c2
    (integer) 3
    > lrange test 0 -1
    1) "c1"
    2) "c2"
    3) "c3"
    > lindex test 1
    "c2"
    > lpop test
    "c1"
    > lrange test 0 -1
    "c2"
    "c2"

Hash(字典)

  • 简介:键值对集合,类似于编程语言中的 Map 类型。
  • 可存储的值:非常适合存储对象,可以像数据库 UPDATE 一样只修改某一项属性值。
  • 操作:添加、获取、移除单个键值对;获取所有键值对;检查某个键是否存在。
  • 应用场景:存储、读取、修改用户属性。
  • 实际使用
    > hset test name cbuc
    (integer) 1
    > hset test age 23
    (integer) 1
    > hgetall test
    1)"name"
    2)"cbuc"
    3)"age"
    4)"23"
    > hdel test age
    (integer) 1
    > hget test name
    "cbuc"
    > hgetall test
    1)"name"
    2)"cbuc"

Set(集合)

  • 简介:基于 Hash 表实现,元素不重复。
  • 可存储的值:无序集合。
  • 操作:添加、获取、移除单个元素;检查元素是否存在;计算交集、并集、差集;随机获取元素。
  • 应用场景:共同好友;利用唯一性统计访问网站的所有独立 IP。
  • 实际使用
    > sadd test c1
    (integer) 1
    > sadd test c2
    (integer) 1
    > sadd test c1
    (integer) 0
    > smembers test
    1)"c1"
    2)"c2"
    > sismember test c3
    (integer) 0
    > sismember test c1
    (integer) 1
    > srem test c1
    (integer) 1
    > smembers test
    1)"c2"

ZSet(有序集合)

  • 简介:在 Set 基础上,为每个元素关联一个权重参数 score,元素按 score 有序排列。
  • 可存储的值:有序集合。
  • 操作:添加、获取、删除元素;根据分值范围或成员获取元素;计算键的排名。
  • 应用场景:排行榜;带权重的消息队列。
  • 实际使用
    > zadd test 92 math
    (integer) 1
    > zadd test 88 english
    (integer) 1
    > zadd test 92 score
    (integer) 1
    > zrange test 0 -1 withscores
    1)"english"
    2)"88"
    3)"math"
    4)"92"
    > zrangebyscore test 80 90 withscores
    1)"english"
    2)"88"
    > zrem test english
    (integer) 1
    > zrange test 0 -1 withscores
    3)"math"
    4)"92"

高级用法

  • BitMap(位图):支持按 bit 位存储信息,可用于实现布隆过滤器 (BloomFilter)。
  • HyperLogLog:提供不精确的去重计数,适合大规模数据的去重统计,如统计 UV(独立访客)。
  • Geospatial:用于保存地理位置,进行距离计算或根据半径计算位置,可实现“附近的人”、最优路径规划等功能。

什么是 Redis 持久化?

持久化是指将内存中的数据写入磁盘,以防止服务宕机导致内存数据丢失。

Redis 的持久化机制

Redis 提供两种持久化机制:RDB(默认)和 AOF。

RDB(Redis DataBase)

RDB 是 Redis 默认的持久化方式。按照一定的时间周期,将内存的数据以快照形式保存到磁盘,生成 dump.rdb 数据文件。快照周期可通过配置文件中的 save 参数定义。

RDB持久化机制示意图

原理:基于 forkcow(Copy-On-Write)。Redis 会定期 fork 出一个子进程,子进程将数据写入一个临时 RDB 文件,写入完成后替换旧的 RDB 文件。这种方式利用了操作系统的写时复制技术。

优点

  • 方便持久化:只有一个文件 dump.rdb,易于管理。
  • 容灾性好:单个文件便于备份到安全磁盘。
  • 性能最大化:由子进程负责持久化 I/O 操作,主进程继续处理命令,保证 Redis 高性能。

缺点

  • 数据安全性低:两次快照之间的数据可能丢失,更适合对数据一致性要求不高的场景。
  • 保存耗时长:如果数据量很大,生成快照的时间会比较长。

AOF(Append-Only-File)

AOF 会将 Redis 执行的每个写命令记录到独立的日志文件中。重启时,Redis 会重新执行 AOF 文件中的命令来恢复数据。

AOF持久化机制示意图

原理:将写命令追加到 AOF 文件末尾。需要通过配置同步选项 (appendfsync) 来控制命令同步到磁盘的时机。

同步选项

选项 同步频率
no 由操作系统决定何时同步
always 每个写命令都同步
everysec 每秒同步一次
  • no:不能显著提升性能,且系统崩溃时数据丢失较多。
  • always:数据安全性最高,但会严重降低服务器性能。
  • everysec:推荐选项。在保证系统崩溃时最多丢失一秒数据的前提下,对服务器性能影响微乎其微。

随着写操作增多,AOF 文件会膨胀。Redis 提供了 AOF 重写(auto-aof-rewrite) 机制,能够去除文件中的冗余命令。

优点

  • 数据安全:可配置为 always,每次写操作都立即记录。
  • 一致性:采用追加写模式,即使服务器中途宕机,也可通过 redis-check-aof 工具修复数据一致性问题。

缺点

  • AOF 文件通常比 RDB 文件大,恢复速度更慢。
  • 数据集较大时,启动效率低于 RDB。

RDB 与 AOF 比较

  • AOF 文件更新频率高于 RDB,数据恢复时优先使用 AOF。
  • AOF 通常比 RDB 更安全、功能更强大(例如记录了所有写操作)。
  • RDB 在恢复大数据集时性能优于 AOF。
  • 如果同时配置了两种方式,Redis 重启时优先加载 AOF 文件恢复数据。

如何选择合适的持久化方式?

建议两者都配置,形成互补。单独使用 RDB 可能导致较多数据丢失;单独使用 AOF,数据恢复速度不如 RDB 快。实践中,可以先利用 RDB 快速恢复数据主体,再通过 AOF 补全增量数据。这种“冷热备份”结合的方式,能构建健壮性更高的系统。

Redis 的过期键删除策略

  1. 定时删除:为每个设置过期时间的 key 创建定时器,到期立即删除。对内存友好,但消耗大量 CPU 资源,影响缓存性能。
  2. 惰性删除:只有当访问一个 key 时,才判断其是否过期,过期则删除。最大化节省 CPU,但对内存不友好,可能导致大量过期 key 未被及时清理。
  3. 定期删除:每隔一段时间,扫描一定数量的过期字典中的 key 并清除已过期的。是前两种策略的折中,可通过调整扫描间隔和耗时来平衡 CPU 和内存资源。

Redis 通常同时使用 惰性删除定期删除 两种策略。

Redis 内存淘汰策略

当内存不足时,Redis 会根据配置的策略淘汰部分数据。

针对设置了过期时间的 key

  • volatile-lru:尝试回收最少使用的键。
  • volatile-random:随机回收键。
  • volatile-ttl:优先回收存活时间较短的键。

针对所有 key

  • allkeys-lru:尝试回收最少使用的键。
  • allkeys-random:随机回收键。
  • noeviction:禁止淘汰数据,当内存不足时,新写入操作会返回错误。

Redis 事务

Redis 事务本质是通过 MULTIEXECWATCHDISCARD 四个原语实现。它支持一次执行多个命令,所有命令会被序列化并顺序执行,且在执行过程中不会被其他客户端命令打断。

特点

  • Redis 不支持回滚:事务中某个命令失败,其余命令仍会继续执行,这是为了保持 Redis 的简单与高效。
  • 如果命令入队时出错(如语法错误),所有命令都不会执行。
  • 如果命令执行时出错(如对错误类型键操作),正确的命令会被执行。

四个原语

  • WATCH:乐观锁,监控一个或多个键。如果在 EXEC 执行前被修改,则事务不会执行。
  • MULTI:开启一个事务,将后续命令放入队列。
  • EXEC:执行事务队列中的所有命令。
  • DISCARD:清空事务队列,并放弃执行事务。

如何设置键的过期时间和永久有效?

使用 EXPIRE key seconds 设置过期时间,使用 PERSIST key 移除过期时间,使其永久有效。

缓存雪崩

定义:缓存中大量数据在同一时间点失效,导致所有请求直接涌向数据库,造成数据库瞬时压力过大而崩溃。

解决方案

  • 将缓存数据的过期时间设置为随机值,避免同时失效。
  • 对于并发量不大的场景,可采用加锁排队的方式控制数据库访问。
  • 搭建 Redis 集群,实现高可用,避免单点故障。

缓存击穿

定义:某个热点 key 在缓存中过期(但数据库中仍有数据),此时有大量并发请求同时访问这个 key,全部穿透到数据库,造成数据库压力骤增。

解决方案

  • 设置热点数据永不过期。
  • 使用互斥锁(如 Redis 分布式锁),保证同一时间只有一个请求去数据库加载数据。
  • 采用随机退避策略,缓存失效时随机等待很短时间再重试查询或更新。

缓存穿透

定义:查询一个缓存和数据库中都不存在的数据。导致每次请求都会落到数据库上,可能压垮数据库。

解决方案

  • 缓存空对象:对于查询不存在的数据,在缓存中保存一个空值(或特殊标记)并设置较短过期时间,防止重复查询数据库。
  • 使用布隆过滤器 (BloomFilter):在查询缓存前,先经过布隆过滤器判断数据是否存在。如果过滤器判断不存在,则数据一定不存在,直接返回;如果判断存在,则数据可能存在(有误判率),再去查询缓存和数据库。

Redis 主从架构

单机 Redis 的 QPS 承载能力有限。为了支撑读高并发,通常采用主从架构(Master-Slave)。Master 节点处理写操作,并将数据同步到多个 Slave 节点,Slave 节点专门负责读操作。这样不仅分摊了读请求压力,也便于水平扩容。

Redis主从架构与读写分离示意图

首次全量同步流程

  1. Slave 启动后,向 Master 发送 PSYNC 命令请求同步。
  2. Master 执行 BGSAVE 生成当前数据的 RDB 快照文件,同时将后续的写命令缓冲在内存中。
  3. RDB 文件生成后,Master 将其发送给 Slave。
  4. Slave 接收并加载 RDB 文件到内存,完成数据初始化。
  5. Master 再将缓冲区的写命令(增量数据)发送给 Slave 进行重放,最终实现主从数据一致。
    后续的增量数据同步则通过类似于数据库 binlog 的 AOF 日志流进行。

Redis 实现分布式锁

一个简单的实现方式是:使用 SETNX (SET if Not eXists) 命令争抢锁,抢到后再用 EXPIRE 命令为锁设置一个过期时间,防止持有者意外崩溃导致锁无法释放。

SETNX key value:仅当 key 不存在时,将其值设为 value。设置成功返回 1,失败(key已存在)返回 0。

Redis 的同步机制

Redis 支持主从同步和从从同步(级联复制)。其同步机制与上述主从架构中的首次全量同步流程一致,核心是 RDB 快照 + 增量命令缓冲与重放。

Redis 集群原理

Redis 提供了两种集群方案,着眼点不同:

  • Redis Sentinel(哨兵):着眼于高可用。当 Master 宕机时,能自动将某个 Slave 提升为新的 Master,继续提供服务。
  • Redis Cluster:着眼于扩展性。当单个 Redis 实例内存不足时,通过分片(Sharding)将数据分布到多个节点存储。

Sentinel 选主策略(当原 Master 故障时):

  1. Slave 优先级(slave-priority)越低,优先级越高。
  2. 优先级相同时,复制偏移量(数据同步程度)越大的 Slave 优先级越高。
  3. 以上都相同时,ID (runid) 越小的 Slave 越容易被选中。

Redis 哨兵模式

Redis哨兵(Sentinel)高可用架构图

哨兵模式通常需要至少三个 Sentinel 实例来保证自身的健壮性。需要注意的是,哨兵+主从复制 并不能保证数据零丢失,但可以保证集群的高可用性。

工作原理

  1. 每个 Sentinel 每秒向所有已知的主、从服务器和其他 Sentinel 发送 PING 命令。
  2. 如果一个实例在配置的 down-after-milliseconds 时间内未有效回复 PING,则被该 Sentinel 标记为主观下线
  3. 如果一个 Master 被标记为主观下线,所有监视它的 Sentinel 会以每秒一次的频率确认其状态。
  4. 当有足够数量(达到配置的法定人数)的 Sentinel 在指定时间范围内都认为该 Master 下线,则将其标记为客观下线
  5. Sentinel 集群会通过投票协议,从该 Master 的 Slaves 中选举出一个新的 Master。
  6. 将其余 Slaves 指向新的 Master 开始复制数据。
  7. 通知客户端主从切换的结果。

脑裂问题及解决

何为脑裂

在 Redis 主从集群中,由于网络分区(Network Partition),导致 Master 节点与其 Slaves 节点以及 Sentinel 集群被隔离在不同的网络区域。Sentinel 集群因无法感知到原 Master 的存在,从而将某个 Slave 提升为新的 Master。此时集群中出现了两个“Master”。如果客户端仍基于原 Master 写入数据,则新 Master 无法同步这些数据。当网络恢复后,原 Master 会被 Sentinel 降级为 Slave 并向新 Master 同步数据,这将导致原 Master 上未被同步的数据永久丢失。

Redis集群脑裂问题示意图

解决方案

  • 采用隔离机制
    • 共享存储 Fencing:确保只有一个 Master 能往共享存储写数据。
    • 客户端 Fencing:确保只有一个 Master 能响应客户端请求。
    • Slave Fencing:确保只有一个 Master 能向 Slave 下发命令。
  • 修改 Redis 配置
    min-replicas-to-write 3  # 要求 Master 至少要有 3 个可写的 Slave 连接
    min-replicas-max-lag 10  # 要求数据复制延迟不能超过 10 秒

    配置这两个参数后,如果发生脑裂,原 Master 因为无法满足“至少有3个延迟小于10秒的Slave”这一条件,会拒绝执行客户端的写请求。这大大减少了网络恢复后数据同步过程中的数据丢失。

理解了这些核心原理,无论是进行高可用架构设计,还是准备技术面试,都能更有把握。当然,纸上得来终觉浅,最好的学习方式还是在实践中不断探索和验证。




上一篇:Redis面试宝典:从基础到高级的30个核心问题解析
下一篇:面向对象业务建模:如何通过抽象与用例分析降低系统复杂度?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 18:53 , Processed in 0.754731 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表