Reids篇

Redis

数据结构

Redis的底层数据结构

常见数据结构:String,Hash,List,Set,Zset(有序集合)

新增数据结构:

  • BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
  • HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
  • GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
  • Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

Zset底层是怎么实现的?

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

跳表是怎么实现的?

跳表是一种在有序链表基础上增加多层索引的有序数据结构,可以让查找、插入和删除的平均时间复杂度从 O(n) 降到 O(logN)

它通过随机算法将部分节点基于一定概率(默认0.25)提升到更高的索引层,形成“跳跃”访问路径。查找时从最高层开始,向右查找直到超过目标值,再向下一层继续,直到到达底层。

Redis 在 zset 中就用跳表配合哈希表实现了按 score 排序的有序集合,跳表负责范围查询,哈希表负责快速定位 score,这样既能 O(1) 查单个元素,也能 O(logN) 查区间,非常高效。

Redis为什么用跳表不是B+树?

  1. 内存结构与访问方式的差异:
    跳表:基于链表更符合内存的访问方式,通过指针建立。
    B+树:更符合磁盘IO优化,
    Reids是内存数据库。
  2. 实现复杂度对比:
    跳表:只用生成层高,删除时移除指针,代码少,200多行。
    B+树:插入修改涉及平衡树的再平衡逻辑,代码多,数千行。
  3. 性能:
    查询性能二者都差不多,单节点查询跳表性能更优,范围查询B+树性能更优。
    插入跳表性能比B+树高。B+树插入数据可能涉及再平衡。
  4. 内存占用:
    跳表用链表结构连接,内存紧凑。B+树存在,节点未被占满的情况,会产生内存碎片。

如何判断几十亿个数中是否存在某个数?

使用BitMap数据结构,BitMap每一位数为1代表该数存在,存储几十亿个数只需要几十亿个位,换算成内存存储为一百多MB。

哈希表怎么扩容的

将哈希表1的key-value迁移到新扩容的哈希表2,此时如果有查询,更新,删除,等操作,先从哈希表1查起,如过没有再到哈希表2查。

Redis中String是用什么存储的?

String数据结构:

len:字符串长度

alloc:分配的空间长度

flags:sds类型

buf:字符数组,用来储存字符串。

相比于C语言的String,Redis的String设计好处

  • 字符串长度查找效率是O(1),因为只用查len就行
  • 二进制安全:Redis因为有len标记字符串长度,所以可以不使用”/0”做字符串结尾。SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。
  • 不会发生缓冲区溢出:因为有len和alloc来判断字符串空间大小,一旦识别出剩余内存空间不足,Redis会自动扩大SDS的空间大小。

线程模型

Redis为什么这么快?

内存操作:

  • 数据存储在内存中,访问速度更快
  • 避免I/o磁盘性能开销
  • 内存访问比磁盘快

高效的数据结构:

  • 针对不同场景的数据结构
  • SDS、压缩列表、跳表等高效实现
  • 减少内存占用和计算复杂度

单线程模型

  • 避免线程上下文切换开销
  • 无需线程同步,避免锁竞争
  • CPU密集型操作效率更高

高I/O多路复用机制:

IO 多路复用机制是指一个线程处理多个 IO 流。在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。

RESP协议设计优势

Redis序列化(RESP)协议

简单高效、管道技术、批量操作

Redis单线程模型?

新版Redis为了解决网络并发问题,提高效率,会创建几个IO线程。默认总体为6个线程:

  • Redis-server:主线程,负责执行命令;
  • bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
  • io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。

Redis怎么实现IO多路复用?

IO多路复用机制,IO多路制的是Redis监听的多个IO,复用指的是复用redis主线程。

img

如上图对 Redis 的 I/O 多路复用模型进行一下描述说明:

  • 一个 socket 客户端与服务端连接时,会生成对应一个套接字描述符(套接字描述符是文件描述符的一种),每一个 socket 网络连接其实都对应一个文件描述符。
  • 多个客户端与服务端连接时,Redis 使用 I/O 多路复用程序 将客户端 socket 对应的 FD 注册到监听列表(一个队列)中。当客服端执行 read、write 等操作命令时,I/O 多路复用程序会将命令封装成一个事件,并绑定到对应的 FD 上。
  • 文件事件处理器使用 I/O 多路复用模块同时监控多个文件描述符(fd)的读写情况,当 accept、read、write 和 close 文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器进行处理相关命令操作。

例如:以 Redis 的 I/O 多路复用程序 epoll 函数为例。多个客户端连接服务端时,Redis 会将客户端 socket 对应的 fd 注册进 epoll,然后 epoll 同时监听多个文件描述符(FD)是否有数据到来,如果有数据来了就通知事件处理器赶紧处理,这样就不会存在服务端一直等待某个客户端给数据的情形。

Redis的网络模型是什么样的?

img

Redis 6.0 版本之前,是用的是单Reactor单线程的模式:

  • 存在缺点无法充分利用 多核 CPU 的性能;Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟

到 Redis 6.0 之后,就将网络IO的处理改成多线程的方式了,目的是为了这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。

事务

Redis如何实现原子性?

  1. 因为Redis是单线程的,单条命令,每条都有原子性。但多条无法保证。
  2. 多条命令情况下使用lua脚本保证事务原子性。

日志

Redis数据持久化方式?

分为两种,一是AOF日志,一种是RDB快照

  • AOF日志是记录每条命令到缓冲区,当出现Redis重启时,系统根据AOF日志执行每条命令,还原数据。

AOF日志分为三种级别,一是Always,AOF缓冲区每条命令都记录在磁盘里。二是Everysec,每秒钟将AOF缓冲区的记录写在磁盘里。三是NO,Redis不执行写回磁盘操作,由操作系统管理。

  • RDB快照就是将数据快照保存下来

分为两种功能,一种是save,在主线程完成,可能阻塞主线程;二是,创建子进程生成RDB文件,避免阻塞主线程

AOF和RDB优缺点?

AOF:

  • 优点:实时保存,更好的数据安全性。即使文件被损坏,也有reids-check-aof工具修复。
  • 缺点:AOF文件占据磁盘空间大。如果文件过大,每次重新载入Redis,数据恢复过程可能要很久。

RDB

  • 优点:体积小。
  • 缺点:安全性低。在手动执行保存快照间,如果Redis出现故障,数据无法保留。

缓存淘汰和过期删除?

内存淘汰和过期键删除的区别

区别:

  • 内存淘汰是指内存满了,触发内促淘汰策略,将不必要的内存腾出。
  • 过期键删除是键设置了过期时间,对已设置过期时间的键进行删除操作。

内存淘汰策略

img

分为两大类,不进行数据淘汰策略进行数据淘汰策略

不进行淘汰策略,默认内存一满,就无法进行写入数据,但还是可以进行查询。

进行数据淘汰策略,分为两种:

设置了过期时间数据淘汰volatile

  • random:随机淘汰
  • ttl:优先淘汰更早过期的
  • lru:淘汰最久未使用的
  • lfu:淘汰使用频率最低的

所有数据范围数据淘汰allkeys

  • random:随机淘汰数据
  • lru:淘汰最久未使用
  • lfu:淘汰最少使用

LRU和LFU

LRU(Least Recently Used):最少最近使用。核心思想:淘汰内存策略,最近被访问的内存,未来被访问到的概率越大,最长时间未被使用的就删除。相当于维护一个双向链表,最先进的先出。

LFU(Least Frequently Used):最少频率使用。核心思想:注重频率,一定时期内被访问次数最少的页,在将来被访问到的几率也是最小的。

过期删除策略

惰性删除+定期删除组成

惰性删除:在访问或修改key之前,会用一个函数expireIfNeeded函数检查是否过期,过期就删除,没有就正常返回键值。

定期删除:间隔一段时间随机从数据库中取出数据检查是否过期,过期就删除。

  • 间隔时间:默认10hz,每秒进行十次过期检查。
  • 过程:从过期字典中取20个key;检查是否过期;若过期占抽样的数据超过0.25,就继续抽样,直到不超过0.25为止。
  • 删除时间:默认,不超过25ms

集群

Redis主从同步中的增量和完全同步怎么实现?

1.什么时候发生完全同步?

从服务器初次连接主服务器;从服务器发生数据丢失;从服务器长时间没有更新,数据和主服务器差距过大,可能发生完全同步

2.如何实现增量同步和完全同步的呢?

同步过程

  1. 从服务器发主服务器sync命令
  2. 主服务器生成RDB快照
  3. 传输RDB文件
  4. 从服务器接收RDB文件
  5. 主服务器记录写文件:在RDB生成或传输期间,主服务器接收到的写记录写入replication backlog buffer缓冲区
  6. 传输写命令:一旦RDB文件传输完成,主服务器会将replication backlog buffer中的命令发送给从服务器,从服务器会执行这些命令,以保证数据的一致性。

如何判断什么时候增量同步和完全同步

  1. 增量同步基于psync命令,传输复制偏移量offset
  2. 主服务器会将写命令写入repl_backlog_buffer环形缓冲区
  3. 从服务器发送记录的复制偏移量slave_repl_offset给主服务器,主服务器对比自己写的偏移量master_repl_offset的差值,来判断执行增量同步还是完全同步。
  4. 两个偏移量差值在repl_backlog_buffer中存在,就执行增量同步;如若缓冲区没有数据,就执行完全同步

为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,我们应该调整repl_backlog_buffer 缓冲区大小,尽可能的大一些。

心跳检测机制

从服务器默认每秒向主服务器发送REPLCONF ACK命令。

汇报自己的复制偏移量。主服务器检测从服务器是否正常。

主服务器对比偏移量,发挥判定结果。

哨兵机制原理

当主节点挂掉了,Redis存在一种机制,智能检测主节点是否挂了,如果挂了就自动切换主服务器,并将新节点的相关信息同步到其他从服务器,这个机制是哨兵机制(Sentinel)

哨兵其实是一个运行在特殊模式下的 Redis 进程,所以它也是一个节点。

主要负责三个事情:监控、选主、通知

哨兵机制的选主节点算法有哪些?

  1. 故障节点主观下线
    Sentinel集群的每一个Sentinel节点会定时redis集群的所有节点心跳包检测节点是否正常。如果一个节点在down-after-milliseconds时间内没有回复Sentinel节点的心跳包,则该redis节点被该Sentinel节点主观下线。
    img

  2. 故障节点客观下线
    当节点被一个Sentinel节点记为主观下线时,并不意味着该节点肯定故障了。该Sentinel节点会询问其他Sentinel节点,如果Sentinel集群中超过quorum数量的Sentinel节点认为该redis节点主观下线,则该redis客观下线。
    img

  3. Sentinel集群选举Leader
    如果需要从redis集群选举一个节点为主节点,首先需要从Sentinel集群中选举一个Sentinel节点作为Leader。
    每一个Sentinel节点都可以成为Leader,当一个Sentinel节点确认redis集群的主节点主观下线后,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意。

  4. Sentinel Leader决定新主节点

    当Sentinel集群选举出Sentinel Leader后,由Sentinel Leader从redis从节点中选择一个redis节点作为主节点:

    1. 过滤故障的节点
    2. 选择优先级slave-priority最大的从节点作为主节点,如不存在则继续
    3. 选择复制偏移量(数据写入量的字节,记录写了多少数据。主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续
    4. 选择runid(redis每次启动的时候生成随机的runid作为redis的标识)最小的从节点作为主节点

Redis Cluster集群模式

当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案。

一个切片集群共有 16384 2^15 个哈希槽,每个key要经过CRC16 算法计算一个 16 bit 的值,将16bit的值与16384取模运算,分配到哪个槽位。

分为两种模式:一是平均分配,将16384个哈希槽平均分配给所有Redis集群;二是手动分配,手动指定每个集群的哈希槽个个数,但是加起来一定得满16384个。

Redis CLuster分配的所有集群都是主节点

Redis集群模式优缺点

优点:

  • 高可用性:Redis节点采用主从复制模式。
  • 高性能。
  • 扩展性好。

缺点:

  • 部署和维护较复杂
  • 集群同步问题
  • 数据分片限制]

Redis Cluster 分片集群间如何通信?

Gossip协议集群管理

  • 集群中每个节点都维护其他节点的信息
  • 通过Gossip协议传播节点状态信息
  • 新节点加入时自动被其他节点发现

故障检测:

  • 节点之间定期发ping
  • 超时被标记PFAIL(probably fail)
  • 超半数的节点都认为该节点故障,标记为FAIL

槽位迁移

  • 支持在线数据迁移
  • 源节点和目标节点协调完成数据搬移
  • 迁移期间保证数据一致性

分片集群的读写规则?

写:

  • 计算keyhash值
  • 找到对应分配的槽位,发送请求
  • 如果请求失败,返回MOVED错误
  • 重定向到正确的槽位

读:

  • 主节点读取:默认
  • 从节点读取:READONLY命令
  • 最近读取:选取网络延迟最低的节点

多KEY操作:

  • 使用Hash Tag 将多个key放在同一哈希槽。

分布式锁

实现方案

  • SET NX EX 但是缺少原子性
  • SET NX EX +Lua脚本 但是存在锁过期
  • Redisson 看门狗机制实现锁续期
  • RedLock算法
    向N个Redis的实例请求锁。
    锁采用相同的key和随机值。
    设置较小的过期时间。
    超半数Redis实例加锁成功才加锁。
    计算加锁总耗时,确保小于过期时间
    优点:更高的安全性
    缺点:复杂度高,性能开销大

场景

如何解决大key问题?

  • 拆分大key
  • 清理大key
  • 监控Redis内存水位
  • 对过期数据定期清理

如何解决热点key问题?

  • 复制热点key到其他集群
  • 使用读写分离架构

如何保证 redis 和 mysql 数据缓存一致性问题?

CAP理论一致性(consistency)可用性(Availability)分区容错性(Partition tolerance)

  1. 采用旁路缓存策略,先读取缓存,缓存没有再读取数据库。
  2. 先更新数据库,再删除缓存。
  3. 消息队列方案,引入消息队列,将删除缓存的操作存入消息队列。
    当缓存删除失败,可以从消息队列中重新读取数据,再删除缓存。
    当缓存删除成功,就把数据从消息队列中移除。
  4. 订阅数据库binlog日志,将binlog日志采集发送到MQ队列里面,然后编写一个简单的缓存删除消息者订阅binlog日志,根据更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存一致性。

如何避免缓存击穿问题?

缓存击穿我们用“互斥回源”和“逻辑过期+后台重建”两套:热点 key 失效时由分布式锁保证只回源一次,其他请求短暂等待;对读多写少的数据采用Stale-While-Revalidate,过期先返回旧值、后台异步刷新,进一步杜绝击穿。再配合本地二级缓存预热/刷新前置、以及极端情况下的热点分片与限流降级,把 DB 峰值压住、可用性拉满。


Reids篇
http://example.com/2025/08/14/Reids篇/
作者
Luogic
发布于
2025年8月14日
许可协议