技术架构系列 - 详解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),使用更快的硬盘,或在从节点执行持久化任务。
相关推荐
虾说羊2 小时前
redis中的哨兵机制
数据库·redis·缓存
_F_y3 小时前
MySQL视图
数据库·mysql
2301_790300963 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
码农水水3 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
九章-3 小时前
一库平替,融合致胜:国产数据库的“统型”范式革命
数据库·融合数据库
-Try hard-3 小时前
数据结构:链表常见的操作方法!!
数据结构·算法·链表·vim
深蓝电商API3 小时前
async/await与多进程结合的混合爬虫架构
爬虫·架构
2401_838472513 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
u0109272713 小时前
使用Python进行网络设备自动配置
jvm·数据库·python