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+树?
- 内存结构与访问方式的差异:
跳表:基于链表更符合内存的访问方式,通过指针建立。
B+树:更符合磁盘IO优化,
Reids是内存数据库。 - 实现复杂度对比:
跳表:只用生成层高,删除时移除指针,代码少,200多行。
B+树:插入修改涉及平衡树的再平衡逻辑,代码多,数千行。 - 性能:
查询性能二者都差不多,单节点查询跳表性能更优,范围查询B+树性能更优。
插入跳表性能比B+树高。B+树插入数据可能涉及再平衡。 - 内存占用:
跳表用链表结构连接,内存紧凑。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主线程。
如上图对 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的网络模型是什么样的?
Redis 6.0 版本之前,是用的是单Reactor单线程的模式:
- 存在缺点无法充分利用 多核 CPU 的性能;Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;
到 Redis 6.0 之后,就将网络IO的处理改成多线程的方式了,目的是为了这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
事务
Redis如何实现原子性?
- 因为Redis是单线程的,单条命令,每条都有原子性。但多条无法保证。
- 多条命令情况下使用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出现故障,数据无法保留。
缓存淘汰和过期删除?
内存淘汰和过期键删除的区别
区别:
- 内存淘汰是指内存满了,触发内促淘汰策略,将不必要的内存腾出。
- 过期键删除是键设置了过期时间,对已设置过期时间的键进行删除操作。
内存淘汰策略
分为两大类,不进行数据淘汰策略和进行数据淘汰策略:
不进行淘汰策略,默认内存一满,就无法进行写入数据,但还是可以进行查询。
进行数据淘汰策略,分为两种:
设置了过期时间数据淘汰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.如何实现增量同步和完全同步的呢?
同步过程:
- 从服务器发主服务器sync命令
- 主服务器生成RDB快照
- 传输RDB文件
- 从服务器接收RDB文件
- 主服务器记录写文件:在RDB生成或传输期间,主服务器接收到的写记录写入replication backlog buffer缓冲区。
- 传输写命令:一旦RDB文件传输完成,主服务器会将
replication backlog buffer
中的命令发送给从服务器,从服务器会执行这些命令,以保证数据的一致性。
如何判断什么时候增量同步和完全同步
- 增量同步基于psync命令,传输复制偏移量offset
- 主服务器会将写命令写入repl_backlog_buffer环形缓冲区。
- 从服务器发送记录的复制偏移量slave_repl_offset给主服务器,主服务器对比自己写的偏移量master_repl_offset的差值,来判断执行增量同步还是完全同步。
- 两个偏移量差值在repl_backlog_buffer中存在,就执行增量同步;如若缓冲区没有数据,就执行完全同步。
为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,我们应该调整repl_backlog_buffer 缓冲区大小,尽可能的大一些。
心跳检测机制
从服务器默认每秒向主服务器发送REPLCONF ACK命令。
汇报自己的复制偏移量。主服务器检测从服务器是否正常。
主服务器对比偏移量,发挥判定结果。
哨兵机制原理
当主节点挂掉了,Redis存在一种机制,智能检测主节点是否挂了,如果挂了就自动切换主服务器,并将新节点的相关信息同步到其他从服务器,这个机制是哨兵机制(Sentinel)。
哨兵其实是一个运行在特殊模式下的 Redis 进程,所以它也是一个节点。
主要负责三个事情:监控、选主、通知。
哨兵机制的选主节点算法有哪些?
故障节点主观下线
Sentinel集群的每一个Sentinel节点会定时对redis集群的所有节点发心跳包检测节点是否正常。如果一个节点在down-after-milliseconds时间内没有回复Sentinel节点的心跳包,则该redis节点被该Sentinel节点主观下线。故障节点客观下线
当节点被一个Sentinel节点记为主观下线时,并不意味着该节点肯定故障了。该Sentinel节点会询问其他Sentinel节点,如果Sentinel集群中超过quorum数量的Sentinel节点认为该redis节点主观下线,则该redis客观下线。Sentinel集群选举Leader
如果需要从redis集群选举一个节点为主节点,首先需要从Sentinel集群中选举一个Sentinel节点作为Leader。
每一个Sentinel节点都可以成为Leader,当一个Sentinel节点确认redis集群的主节点主观下线后,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意。Sentinel Leader决定新主节点
当Sentinel集群选举出Sentinel Leader后,由Sentinel Leader从redis从节点中选择一个redis节点作为主节点:
- 过滤故障的节点
- 选择优先级slave-priority最大的从节点作为主节点,如不存在则继续
- 选择复制偏移量(数据写入量的字节,记录写了多少数据。主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续
- 选择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)
- 采用旁路缓存策略,先读取缓存,缓存没有再读取数据库。
- 先更新数据库,再删除缓存。
- 消息队列方案,引入消息队列,将删除缓存的操作存入消息队列。
当缓存删除失败,可以从消息队列中重新读取数据,再删除缓存。
当缓存删除成功,就把数据从消息队列中移除。 - 订阅数据库binlog日志,将binlog日志采集发送到MQ队列里面,然后编写一个简单的缓存删除消息者订阅binlog日志,根据更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存一致性。
如何避免缓存击穿问题?
缓存击穿我们用“互斥回源”和“逻辑过期+后台重建”两套:热点 key 失效时由分布式锁保证只回源一次,其他请求短暂等待;对读多写少的数据采用Stale-While-Revalidate,过期先返回旧值、后台异步刷新,进一步杜绝击穿。再配合本地二级缓存、预热/刷新前置、以及极端情况下的热点分片与限流降级,把 DB 峰值压住、可用性拉满。