技术架构系列 - 详解Redis

1. Redis知识脑图

2. Redis的架构与核心价值

Redis(Remote Dictionary Server)本质上是一个基于内存的、键值对存储系统 。其设计的核心目标是解决传统关系型数据库在高并发、低延迟场景下的性能瓶颈。它的核心价值可以概括为" "与""。

  • :数据完全存储于内存,读写操作时间复杂度通常为O(1),避免了磁盘I/O的延迟,性能可比磁盘数据库高出数个数量级。
  • :支持丰富的数据结构(String, Hash, List, Set, Sorted Set, Stream等),并基于这些数据结构提供了原子性操作,使其不仅能做简单缓存,还能直接实现复杂的业务逻辑,如排行榜、消息队列等。

从架构视角看,Redis在系统中常扮演 "加速层""状态中间件" 的角色。下图勾勒了其核心逻辑架构:

架构解读

  • 自下而上 :请求从网络层进入,经协议层解析为Redis命令,由单线程事件循环引擎 顺序执行。引擎调用数据结构层(对象系统)完成具体操作,并根据配置决定是否触发持久化或集群同步。
  • 设计核心
    1. 单线程事件模型:避免了多线程的锁竞争和上下文切换,配合I/O多路复用,在绝大多数场景下实现了极高的吞吐量和低延迟。
    2. 对象系统 :这是Redis灵活性的根源。所有数据都被封装为redisObject,通过typeencoding字段实现多态,根据数据大小和类型动态选择最合适的底层数据结构,在性能和内存之间取得平衡。
    3. 分层解耦:各层职责清晰,使得网络协议、持久化策略、集群方案等可以独立演进(如RESP协议、从RDB/AOF到混合持久化、从哨兵到Cluster)。

3. 实现原理

3.1 高效的数据结构设计

Redis的卓越性能源于其精心设计的底层数据结构。它们并非直接暴露给用户,而是通过对象系统进行封装和调用

3.1.1 对象系统(redisObject)

所有Redis键值对中的"值"都是一个redisObject结构体。

objectivec 复制代码
typedef struct redisObject {
    unsigned type:4;     // 数据类型: string, list, hash, set, zset
    unsigned encoding:4; // 编码: int, embstr, raw, hashtable, ziplist...
    unsigned lru:LRU_BITS; // 最近访问时间,用于内存淘汰
    int refcount;        // 引用计数,用于内存回收
    void *ptr;           // 指向实际底层数据结构的指针
} robj;
  • type vs encodingtype是面向用户的(如HSET操作的是Hash类型),而encoding是内部实现(同一个Hash类型,在小数据量时可能用ziplist编码以节省内存,大数据时转为hashtable编码以提升性能)。这种分离是Redis能在不同场景下自动优化的关键。

3.1.2 简单动态字符串(SDS)

Redis没有使用C语言原生的char*字符串,而是自研了SDS。

复制代码
SDS 结构示例 (sdshdr8):
+---------+---------+---------+-----------------+
|  len=5  | alloc=5 | flags=1 | buf[]           |
+---------+---------+---------+-----------------+
                   |         |
                   v         v
                 "H" "e" "l" "l" "o" "\0"
  • 优势
    • O(1)获取长度 :通过len字段直接获得,无需像C字符串那样遍历(O(n))。
    • 杜绝缓冲区溢出 :修改前会检查alloc,自动扩容。
    • 减少内存重分配 :采用空间预分配 (扩容时多分配一些)和惰性空间释放(缩容时不立即归还内存)策略。
    • 二进制安全buf中可以存储包含\0的数据(如图片),通过len判断结束。

3.1.3 字典(Dict)

字典是Redis的基石,用于实现数据库键空间(redisDb)和Hash数据类型。其核心挑战在于扩容时如何避免服务停顿。

复制代码
初始状态 (ht:
索引: 0     1     2     3
      ->K1 ->K2
      ->K5
扩容触发,开始rehash (rehashidx=0):
ht[0] (size=4):            ht[1] (size=8):
索引0: ->K1 ->K5  迁移至>  索引0: ->K1
索引1: ->K2               索引1: ->K2
                          ... (K5根据新size重哈希到索引5)
rehashidx++,逐步迁移...
  • 渐进式Rehash :Redis同时维护两个哈希表ht[0]ht[1]。扩容时,将rehashidx从0开始递增,每次对客户端的增删改查操作,除了执行指定操作,还会顺带将ht[0]rehashidx索引上的所有键值对迁移到ht[1]。迁移完成后,ht[1]替换ht[0]。这巧妙地将庞大的迁移开销分摊到了多次请求中,保证了服务的平滑性。

3.1.4 跳跃表(SkipList)

跳跃表是有序集合(ZSET)的底层实现之一,提供高效的范围查询。

复制代码
头节点 header
  |
L3|--------->[ ]----------------------------->[o3](score=3.0)
  |
L2|----->[ ]--------->[o2](score=2.0)------->[ ]
  |
L1|->[o1](score=1.0)->[ ]->[ ]->[o3](score=3.0)
  • 工作原理 :每个节点有随机层高(level)。查询时从最高层开始,像使用多级索引的尺子,快速跳过大量节点,逐步逼近目标,平均时间复杂度为O(log N)。实现比平衡树更简单,且性能相当。

3.1.5 压缩列表(Ziplist)与快速列表(Quicklist)

  • Ziplist :为极致节省内存设计,是一块连续内存,存储多个元素。但插入删除可能引发"连锁更新"问题。在Redis 7.0中,它被更稳健的Listpack所取代。
  • Quicklist :List类型的默认实现,是双向链表和Ziplist的结合体。它将长列表分段存储为多个Ziplist节点,在保留链表高效插入删除特性的同时,利用Ziplist减少内存碎片和提高缓存局部性。

3.2 单线程事件驱动模型

Redis 6.0之前,其网络I/O和命令执行是单线程的。这并非劣势,而是精妙的设计选择:

  • 避免锁开销:所有数据操作无需加锁,杜绝了竞争条件和死锁。
  • 避免上下文切换:单线程模型使得CPU缓存利用率更高。
  • 配合I/O多路复用:通过epoll、kqueue等系统调用,单线程可以高效管理成千上万的连接,当某个套接字准备好时,事件循环才进行处理,CPU不会空转。

Redis 6.0引入的多线程仅用于处理网络I/O的读写,即解析请求和打包回复,而核心的命令执行逻辑仍然是单线程的,从而保持了串行化的无锁优势。

3.3 持久化

为解决内存数据易失的痛点,Redis提供了两种持久化策略:

  • RDB (快照) :在特定时间点将内存数据全量二进制压缩保存到磁盘(dump.rdb)。恢复快,但可能丢失最后一次快照后的数据。
  • AOF (追加日志) :记录每一个写操作命令,以文本协议格式追加到文件(appendonly.aof)。数据完整性高,但文件体积大,恢复慢。Redis提供AOF重写机制来压缩文件。

通常采用混合模式:定期执行RDB作为基础备份,期间使用AOF记录增量操作,兼顾恢复速度和数据安全。

3.4 高可用与扩展

  • 主从复制 (Replication) :一个主节点(Master)可拥有多个从节点(Slave)。数据从主节点异步同步到从节点,实现了数据备份读写分离(读请求可分流至从节点)。
  • 哨兵模式 (Sentinel) :在主从复制基础上,引入独立的哨兵进程集群,用于监控 主节点状态,并在主节点故障时,自动进行故障转移,选举新的主节点,实现高可用。
  • 集群模式 (Cluster) :Redis的分布式解决方案。采用去中心化架构,将数据分片 到16384个槽位(slot)中,由多个主节点分别负责一部分槽位,每个主节点还可配置从节点。数据访问通过CRC16哈希计算键所属槽位,并直接路由到对应节点,实现了水平扩展和高可用。

4. 典型使用场景与解决的痛点

4.1 缓存

  • 痛点:热点数据频繁查询数据库,导致数据库负载过高,响应变慢。
  • 解决方案:将查询结果存入Redis,后续请求直接命中缓存。例如,电商首页的热门商品列表、文章详情等。设置合理的过期时间或通过消息队列监听数据库变更来更新缓存,以保证数据最终一致性。

4.2 会话存储 (Session Store)

  • 痛点:在Web集群中,用户会话需要共享,无法依赖单机内存。
  • 解决方案:将用户Session(如登录信息)集中存储在Redis中,所有应用服务器均可访问,实现无状态服务架构。

4.3 排行榜/计数器

  • 痛点:实时更新和查询排名,数据库操作频繁且复杂。
  • 解决方案 :使用Sorted Set,成员即条目,分数即排序依据。ZINCRBY命令可原子性更新分数,ZRANGE命令可高效获取排名,完美契合榜单需求。

4.4 消息队列

  • 痛点:需要解耦系统组件,进行异步处理。
  • 解决方案 :使用ListLPUSH/BRPOP命令实现简单的队列。Redis 5.0后提供的Stream数据类型,支持多消费者组、消息持久化和回溯,是更完善的消息队列实现。

4.5 实时数据处理 (Stream Processing)

  • 痛点:实时产生的数据流(如用户点击、物联网传感器数据)需要被即时处理和分析。
  • 解决方案 :利用Stream数据结构,生产者XADD消息,消费者通过XREAD或消费组XREADGROUP进行实时处理,可用于实时统计、监控等场景。

4.6 分布式锁(Distributed Lock)

利用SET命令的NX(不存在才设置)和PX(过期时间)参数实现。

5. 常见问题与解决方案

5.1 缓存经典三问题

  • 缓存穿透 :查询一个根本不存在 的数据,请求穿透缓存,直击数据库。
    • 解决方案 :① 布隆过滤器 (Bloom Filter) :将所有可能存在的key哈希到一个位图中,查询前先过滤,拦截绝大部分无效请求。② 缓存空值 :即使数据库没有,也将这个空结果(如null)进行短时间缓存。
  • 缓存击穿 :某个热点key过期 的瞬间,大量并发请求同时击穿缓存,直达数据库。
    • 解决方案互斥锁 (Mutex Lock) 。第一个发现缓存失效的线程,使用SETNX命令获取分布式锁,然后去数据库加载数据,其他线程等待或重试。加载完成后释放锁,其他线程即可从缓存获取数据。
  • 缓存雪崩大量key在同一时间 或短时间内集中过期,导致所有请求涌向数据库。
    • 解决方案 :① 差异化过期时间 :在基础过期时间上,增加一个随机值(如基础时间 + random(0, 300秒)),打散过期点。② 数据预热 :在系统低峰期提前加载热点数据。③ 保证高可用:采用Redis集群,即使部分节点不可用,服务仍能维持。

5.2 大Key与热Key问题

  • 大Key (BigKey) :指单个key存储的value体积过大(如几百KB的String,包含百万成员的Set)。导致操作耗时、网络阻塞、内存不均,甚至引发集群迁移故障。
    • 解决方案 :① 拆分 :将大Hash拆分为多个小Hash,通过key命名规则关联。② 压缩 :对value进行序列化压缩。③ 使用合适数据结构 :例如,存储用户粉丝ID列表,用Set可能很大,改用Bloom Filter表示可能更节省空间。
  • 热Key (HotKey) :某个key被极高频率地访问,超过单节点处理能力,可能造成该节点CPU过载。
    • 解决方案 :① 本地缓存 :在应用层使用JVM本地缓存(如Caffeine)做二次缓存,但需注意一致性。② Key分片 :将热key拆分为多个子key,如hotkey:1, hotkey:2,分散到不同节点或实例。

5.3 数据一致性与并发竞争

  • 双写一致性 :缓存与数据库的数据同步时机问题。
    • 解决方案 :采用 "先更新数据库,再删除缓存" 的策略。删除失败可通过消息队列重试,保证最终一致性。对于强一致性要求极高的场景,缓存可能不适用。
  • 并发竞争Key :多个客户端同时写同一个key。
    • 解决方案 :使用分布式锁控制写操作的顺序。对于有时序要求的写操作,可在数据中附带时间戳版本,只有版本更新的写操作才被执行。

5.4 运维与性能问题

  • 内存溢出 (OOM) :数据量超过maxmemory限制。
    • 解决方案 :配置合理的maxmemory-policy淘汰策略(如allkeys-lru, volatile-ttl),并监控内存使用情况,及时扩容或优化数据。
  • 持久化阻塞 :RDB fork或AOF重写时,如果内存过大,可能导致主线程短暂阻塞。
    • 解决方案:控制单实例内存大小(建议不超过10GB),使用更快的硬盘,或在从节点执行持久化任务。
相关推荐
苦藤新鸡1 天前
64 搜索平移递增数组中的元素
数据结构·算法
tod1131 天前
Redis 分布式锁进阶:从看门狗到 Redlock 的高可用实践
数据库·redis·分布式
山岚的运维笔记1 天前
SQL Server笔记 -- 第69章:时态表
数据库·笔记·后端·sql·microsoft·sqlserver
Vic101011 天前
链表算法三道
java·数据结构·算法·链表
二年级程序员1 天前
一篇文章掌握“栈”
c语言·数据结构
闲人编程1 天前
Celery分布式任务队列
redis·分布式·python·celery·任务队列·异步化
一只理智恩1 天前
向量数据库在AI领域的核心作用、优势与实践指南
数据库·人工智能
那个松鼠很眼熟w1 天前
1.JDBC程序的一般步骤
数据库
亓才孓1 天前
【SQLSyntaxErrorException】SQL语法错误
数据库·sql·mybatis
一个响当当的名号1 天前
lectrue16 二阶段锁
jvm·数据库