Redis技术栈梳理
2024-11-18 14:05:53 0 举报
AI智能生成
Redis是一个开源的面向键值对的存储系统,它提供了五种核心数据类型:字符串、哈希、列表、集合和有序集合。这些类型支持多种操作,如追加、删除、计算交集等。此外,Redis还支持pub/sub、lua脚本、事务和持久化等功能。作为内存数据库,Redis具有高性能和低延迟的特点,广泛应用于缓存、会话存储、计数器、实时分析等领域。
作者其他创作
大纲/内容
Redis高性能
基于内存实现
内存是由CPU直接交互
单线程模型:避免不必要的线程切换和竞争条件
不会因为线程创建导致的性能消耗;
避免上下文切换引起的 CPU 消耗,没有多线程切换的开销;
避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁问题。
使用I/O多路复用,非阻塞IO
Redis 采用 I/O 多路复用技术,并发处理连接。采用了 epoll + 自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,
多路指的是多个 socket 连接,复用指的是复用一个线程。
基本原理:内核不是监视应用程序本身的连接,而是监视应用程序的文件描述符
Select
Pool
Epool
高效的数据结构
Redis hash词典
Redis 整体就是一个 哈希表来保存所有的键值对,无论数据类型是 5 种的任意一种。哈希表,本质就是一个数组,每个元素被叫做哈希桶,不管什么数据类型,每个桶里面的 entry 保存着实际具体值的指针。
hash冲突的解决:Redis 通过链式哈希解决冲突,Redis 为了追求快,使用了两个全局哈希表。用于 rehash 操作,增加现有的哈希桶数量,减少哈希冲突。rehash的过程并不是一次性的将 hash 表 1 的数据重新映射到 hash 表 2。而是采用了渐进式 rehash,每次处理客户端请求的时候,先从 hash 表 1 中第一个索引开始,将这个位置的 所有数据拷贝到 hash 表 2 中,
SDS 简单动态字符
对比C语言字符串
O(1) 时间复杂度获取字符串长度
C 语言字符串,需要遍历整个字符串时间复杂度为 O(n),C 字符串遍历时遇到 '\0' 时结束。
SDS 中 len 保存这字符串的长度,O(1) 时间复杂度。
空间预分配:SDS 被修改后,程序不仅会为 SDS 分配所需要的必须空间,还会分配额外的未使用空间。
len 的长度小于 1M,那么程序将分配和 len 相同长度的未使用空间。
SDS 修改后 len 长度大于 1M,那么程序将分配 1M 的未使用空间。
空间惰性释放:SDS 进行缩短操作时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面如果需要 append 操作,则直接使用 free 中未使用的空间,减少了内存的分配
二进制安全:在 Redis 中不仅可以存储 String 类型的数据,也可能存储一些二进制数据
zipList 压缩列表:List 、hash、 sorted Set 三种数据类型底层实现之一
数据量小,且都是小整数值
长度短的字符串
LinkedList双端链表
双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的复杂度都是 O(1)。
无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点。
带表头指针和表尾指针:通过 list 结构的 head 指针和 tail 指针,程序获取链表的表头节点和表尾节点的复杂度为 O(1)。
带链表长度计数器:程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数,程序获取链表中节点数量的复杂度为 O(1)。
多态:链表节点使用 void* 指针来保存节点值,并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
quicklist
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
skipList 跳跃表
跳表在链表的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定
整数数组(intset)一个集合只包含整数值元素,并且这个集合的元素数量不多时
根据实际数据类型选择合适的编码
Redis 使用对象(redisObject)来表示数据库中的键值,创建是会创建两个对象
键值对的键对象
键值对的值对象
String
数字,采用 int 类型的编码
非数字的话,采用 raw 编码
List
字符串长度 < 64 字节且元素个数 < 512 ziplist 编码
linkedlist
Hash
Hash 对象保存的所有键值对的键和值的字符串长度均小于 64 字节,且Hash 对象保存的键值对数量小于 512 个 使用zipList
hashtable hash表
Set
元素为整数且元素个数小于一定范围使用 intset 编码
hashtable hash表
ZSet
Zset 保存的元素个数小于 128,且Zset 元素的成员长度都小于 64 字节 使用zipList
Siplist
Redis数据删除策略
惰性删除
当客户端查询对应的数据时,Redis 判断该数据是否过期,过期则删除
定期删除
Redis 通过定时任务删除过期数据。
Redis高可用
RDB和AOF
RDB 内存快照
就是 Redis 内存中的数据在某一刻的状态数据。
快照执行策略
save
主线程执行,会阻塞
bgsave
fork产生一个子进程用于写入 RDB 文件,快照持久化完全交给子进程来处理,父进程继续处理客户端请求,生成 RDB 文件的默认配置
写时复制
bgsave 子进程可以共享主线程的所有内存数据,读取主线程的数据并写入到 RDB 文件。
优缺点
采用二进制 + 数据压缩的方式写磁盘,文件体积小,快照的恢复速度快,
文件频率不好把握,频率过低宕机丢失的数据就会比较多;太快,又会消耗额外开销
AOF 写后日志
Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。
写回磁盘策略
fsync/fdatasync 同步写入,数据不会修
appendfsync
always:同步写回
everysec:每秒写回
no:操作系统控制,
优缺点
记录redis写指令,不会丢数据
数据恢复需要重放指令,恢复较慢
AOF日志重写机制
指令:bgrewriteaof
原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了
AOF 重写也有一个重写日志,为什么它不共享使用 AOF 本身的日志呢?
一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。
如果 AOF 重写过程中失败了,那么原本的 AOF 文件相当于被污染了,无法做恢复使用
Redis 4.0以后混合日志模型
将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
Redis Replication主从原理
数据同步原理
第一次主从全力同步
连接建立
从库配置主库的ip和端口
从库执行 replicaof 并发送 psync 命令,表示要执行数据同步
主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。
主库数据同步到从库
master 执行 bgsave命令生成 RDB 文件,并将文件发送给从库
同时主库为每一个 slave 开辟一块 replication buffer 缓冲区记录从生成 RDB 文件开始收到的所有写命令
从库收到 RDB 文件后保存到磁盘,并清空当前数据库的数据,再加载 RDB 文件数据到内存中。
发送同步期间新写命令到从库
从节点加载 RDB 完成后,主节点将 replication buffer 缓冲区的数据发送到从节点,Slave 接收并执行,从节点同步至主节点相同的状态。
replication buffer(master 端上创建的缓冲区)
存放哪些时间端数据
master 执行 bgsave 产生 RDB 的期间的写操作
master 发送 rdb 到 slave 网络传输期间的写操作;
slave load rdb 文件把数据恢复到内存的期间的写操作。
replication buffer 太小会引发的问题
太小会导致主从复制连接断开。
主从复制连接断开,导致主从上出现重新执行 bgsave 和 rdb 重传操作无限循环。
为什么使用RDB。不是AOF?
二进制文件,网络传输和磁盘写入效率高
从库进行数据恢复的时候,RDB 的恢复效率也要高于 AOF
主从运行期间的同步
基于长连接的命令传播
命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING 和 REPLCONF ACK。
主->从:PING,判断从节点是否超市
从->主:REPLCONF ACK
检测主从服务器的网络连接状态
辅助实现 min-slaves 选项。
主从库网络断开重连
Redis 2.8 之前采用全量复制,2.8以后采增量复制
增量复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效
repl_backlog_buffer 缓冲区
repl_backlog_buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。
master 使用 master_repl_offset记录自己写到的位置偏移量,slave 则使用 slave_repl_offset记录已经读取到的偏移量。
增量复制的流程
slave 会先发送 psync 命令给 master,同时将自己的 runID,slave_repl_offset发送给 master。
master 只需要把 master_repl_offset与 slave_repl_offset之间的命令同步给从库即可。
repl_backlog_buffer 太小的话从库还没读取到就被 Master 的新写操作覆盖了?
一旦被覆盖就会执行全量复制
调整repl_backlog_size
计算公式:repl_backlog_buffer = 2*second * write_size_per_second
second:从服务器断开重连主服务器所需的平均时间
write_size_per_second:master 平均每秒产生的命令数据量大小
哨兵原理
哨兵是 Redis 的一种运行模式,它专注于对 Redis 实例(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主及主从切换,实现故障转移,确保整个 Redis 系统的可用性
功能
监控:持续监控 master 、slave 是否处于预期工作状态。
主观下线 sdown
如果哨兵ping redis节点,redis未响应,且redis节点是 slave节点。就标记为 sdown
客观下线 odown
如果哨兵集群ping redis节点,redis未响应,且redis节点是 master节点。过半的哨兵(Quorum配置)都任务master节挂了,就会标记为odown
自动切换主库:当 Master 运行故障,哨兵启动自动故障恢复流程:从 slave 中选择一台作为新 master
redis实例是正常运行的,且主从间网络是正常的
slave 优先级,通过 slave-priority 配置项,
slave_repl_offset与 master_repl_offset进度差距
slave runID,在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,
通知:让 slave 执行 replicaof ,与新的 master 同步;并且通知客户端与新 master 建立连接。
工作原理
哨兵间的集群通信
Redis 的 pub/sub 发布/订阅机制。
master 有一个 __sentinel__:hello 的专用通道,用于哨兵之间发布和订阅消息。
哨兵和salve节点间通信
Info命令,从master获取salve节点
哨兵和客户端通信
Redis 的 pub/sub 发布/订阅机制。
Redis Cluster
Redis 集群是一种分布式数据库方案,集群通过分片(sharding)来进行数据管理(「分治思想」的一种实践),并提供复制和故障转移功能。集群间 通过Gossip协议相互交互集群信息
将数据划分为 16384 的 slots,每个节点负责一部分槽位。槽位的信息存储于每个节点中。
Key 与哈希槽映射过程可以分为两大步骤
据键值对的 key,使用 CRC16 算法,计算出一个 16 bit 的值;
16 bit 的值对 16384 执行取模,得到 0 ~ 16383 的数表示 key 对应的哈希槽。
Cluster 还允许用户强制某个 key 挂在特定槽位上,可以通过键入tag标记
故障检测
Redis 集群节点采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变
故障转移
当一个 Slave 发现自己的主节点进入已下线状态后,从节点将开始对下线的主节点进行故障转移。
从下线的 Master 及节点的 Slave 节点列表选择一个节点成为新主节点。
新主节点会撤销所有对已下线主节点的 slot 指派,并将这些 slots 指派给自己。
新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
新的主节点开始接收处理槽有关的命令请求,故障转移完成。
选主流程
集群的配置纪元 +1,是一个自增计数器,初始值 0 ,每次执行故障转移都会 +1
检测到主节点下线的从节点向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。
如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
客户端怎么确定访问的数据到底分布在哪个实例上?
Redis 实例会将自己的哈希槽信息通过 Gossip 协议发送给集群中其他的实例,每个实例都有hash槽和实例之间的映射关系
客户端连接集群中的redis实例,通过将 key 通过 CRC16 计算出一个值再对 16384 取模得到对应的 Slot,
定位到槽以后还需要进一步定位到该 Slot 所在 Redis 实例
哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变?
Redis Cluster 提供了重定向机制
MOVED 错误(负载均衡,数据已经迁移到其他实例上)
ASK 错误:slot槽正常迁移,
Redis应用
Bitmap
Bitmap 的底层数据结构用的是 String 类型的 SDS 数据结构来保存位数组,
HyperLogLog
HyperLogLog 是一种不精确的去重基数方案,它的统计规则是基于概率实现的,标准误差 0.81%,这样的精度足以满足 UV 统计需求了
分布式锁
使用SETNX指令,保证加锁成功,DEL无法保证原子性
消息队列
优化redis内存
键值对优化
使用单词简写方式优化内存占用。
过滤不必要的数据
精简数据
数据压缩
使用性能好,内存占用小的序列化方式
Redis新特性
6.0
多线程处理网络 IO;
原因:写网络的 read/write 系统调用占用了Redis 执行期间大部分CPU 时间,瓶颈主要在于网络的 IO 消耗。
优化方向
提高网络 IO 性能,典型的实现比如使用 DPDK来替代内核网络栈的方式。
放弃原因:添加对用户态网络协议栈的支持,需要修改 Redis 源码中和网络相关的部分
使用多线程充分利用多核,提高网络请求读写的并行度,典型的实现比如 Memcached。
Redis 多 IO 线程模型只用来处理网络读写请求,对于 Redis 的读写命令,依然是单线程处理。
过程:将主线程 IO 读写任务拆分出来给一组独立的线程处理,使得多个 socket 读写可以并行化,但是 Redis 命令还是主线程串行执行
主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列;
主线程通过轮询将可读 socket 分配给 IO 线程;
主线程阻塞等待 IO 线程读取 socket 完成;
主线程执行 IO 线程读取和解析出来的 Redis 请求命令;
主线程阻塞等待 IO 线程将指令执行结果回写回 socket完毕;
主线程清空全局队列,等待客户端后续的请求。
客户端缓存;
实现原理:基于tracking
RESP3 协议版本
普通模式
当tracking开启时, Redis会记住每个客户端请求的 key,当 key的值发现变化时会发送失效信息给客户端
失效信息可以通过 RESP3协议发送给请求的客户端,或者转发给一个不同的连接 (支持 RESP2 + Pub/Sub) 的客户端。
Server 端将 Client 访问的 key以及该 key 对应的客户端 ID 列表信息存储在全局唯一的表(TrackingTable),当表满了,回移除最老的记录,同时触发该记录已过期的通知给客户端。
每个 Redis 客户端又有一个唯一的数字 ID,TrackingTable 存储着每一个 Client ID,当连接断开后,清除该 ID 对应的记录。
TrackingTable 表中记录的 Key 信息不考虑是哪个 database 的,虽然访问的是 db1 的 key,db2 同名 key 修改时会客户端收到过期提示,但这样做会减少系统的复杂性,以及表的存储数据量。
TrackingTable
它的数据类型是基数树 ( radix tree)。
服务端对于记录的 key 只会报告一次 invalidate 消息
只有下次客户端再次执行只读命令被 track,才会进行下一次消息通知 。
广播模式
服务端会给客户端广播所有 key 的失效情况,如果 key 被频繁修改,服务端会发送大量的失效广播消息,
实际使用中,一般使用带有前缀的key
RESP2 协议版本的转发:转发模式
细粒度权限控制(ACL);
RESP3 协议的使用;
用于复制的 RDB 文件不在有用,将立刻被删除;
RDB 文件加载速度更快;
7.0
Redis事务
事务特性 ACID
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
指令
MULTI
EXEC
DISCARD
WATCH
流程
开启事务:MULTI
命令入队
执行事务或丢弃
EXEC执行队列指令
DISCARD丢弃队列指令
Redis 事务满足 ACID?
原子性
EXEC 执行前报错保障原子性
EXEC 执行后报错无法保障原子性
EXEC执行时报错,可以通过修改AOF文件保障原子性
一致性
EXEC 执行前,入队报错 可以保证一致性
EXEC 执行后,实际执行时报错可以保障一致性,成功的命令执行,失败的命令失败
EXEC 执行时,实例故障可以保障一致性
隔离性
并发操作在 EXEC 命令前执行,隔离性需要通过 WATCH 机制保证;
并发操作在 EXEC 命令之后,隔离性可以保证。
WACH机制
在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了
持久性
无法保证持久性
解决Redis变慢
网络通信导致的延迟
使用 pipeline 将多个命令连接在一起来减少网络响应往返次数(RTT)。
禁用慢指令
expires 淘汰过期数据
避免同一时间有多个key失效,导致定时删除变慢
bigkey
对大 key 拆分
异步清理大 key
UNLINK 命令
Redis内存淘汰机制
noeviction
不执行淘汰策略
volatile-lru
在设置了过期时间的键空间中,挑选最近最少使用的键进行删除,以腾出空间给新的数据。
volatile-lfu
在设置了过期时间的键空间中,使用频率计数来识别最不经常使用的键。
volatile-random
在设置了过期时间的键空间中,随机选择一些键进行删除
volatile-ttl
在设置了过期时间的键空间中,挑选剩余存活时间(TTL)最短的键进行删除。
allkeys-lru
在整个键空间中,挑选最近最少使用的键进行删除,
allkeys-lfu
在整个键空间(不管是否设置过期时间)中,基于键的访问频率计数来挑选最不经常使用的键进行删除。
allkeys-random
在整个键空间中,随机选择一些键进行删除,同样不考虑键是否设置过期时间。
LRU算法
常见问题
缓存击穿
高并发流量,访问的这个数据是热点数据,请求的数据在 DB 中存在,但是 Redis 存的那一份已经过期,后端需要从 DB 从加载数据并写到 Redis。
关键字:单一热点数据、高并发、数据失效
解决方案
过期时间 + 随机值
预热:预先把热门数据提前存入 Redis 中,并设热门数据的过期时间超大值。
使用锁
缓存穿透
意味着有特殊请求在查询一个不存在的数据,即数据不存在 Redis 也不存在于数据库。
解决方案
缓存空值:当请求的数据不存在 Redis 也不存在数据库的时候,设置一个缺省值(比如:None)。当后续再次进行查询则直接返回空值或者缺省值。
布隆过滤器:在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中,当请求的 id 不存在布隆过滤器中则说明该请求查询的数据一定没有在数据库中保存,就不要去数据库查询了。
布隆过滤器原理
原理
首先分配一块内存空间做 bit 数组,数组的 bit 位初始值全部设为 0。
加入元素时,采用 k 个相互独立的 Hash 函数计算,然后将元素 Hash 映射的 K 个位置全部设置为 1。
检测 key 是否存在,仍然用这 k 个 Hash 函数计算出 k 个位置,如果位置全部为 1,则表明 key 存在,否则不存在。
哈希函数会出现碰撞,所以布隆过滤器会存在误判?
对于 BloomFilter 判断不存在的 key ,则是 100% 不存在的,反证法,如果这个 key 存在,那它每次 Hash 后对应的 Hash 值位置肯定是 1,而不会是 0。布隆过滤器判断存在不一定真的存在。
不允许删除元素
缓存雪崩
大量的请求无法在 Redis 缓存系统中处理,请求全部打到数据库,导致数据库压力激增,
原因
大量热点数据同时过期,导致大量请求需要查询数据库并写到缓存;
Redis 故障宕机,缓存系统异
解决方案
过期时间添加随机值
接口限流
服务熔断和接口限流;
建高可用缓存集群系统。
Redis.mysql的双写一致性
缓存使用策略
Cache-Aside Pattern(旁路缓存))
取缓存、读取数据库和更新缓存的操作都在应用系统来完成,业务系统最常用的缓存策略。
读数据
流程
当应用程序需要从数据库读取数据时,先检查缓存数据是否命中。
如果缓存未命中,则查询数据库获取数据,同时将数据写到缓存中,以便后续读取相同数据会命中缓存,最后再把数据返回给调用者。
如果缓存命中,直接返回。
优缺点
实现简单,并且能获得性能提升。
由于数据仅在缓存未命中后才加载到缓存中,因此初次调用的数据请求响应时间会增加一些开销,因为需要额外的缓存填充和数据库查询耗时。
更新数据
流程
写数据到数据库
将缓存中的数据失效或者更新缓存数据,最常用,删除缓存使缓存数据失效。
不更新缓存的原因
性能问题
安全问题
Read-Through Pattern(直读)
当缓存未命中,也是从数据库加载数据,同时写到缓存中并返回给应用系统。
Write-Through Pattern(同步直写)
Read-Through 类似,发生写请求时,Write-Through 将写入责任转移到缓存系统,由缓存抽象层来完成缓存数据和数据库数据的更新
Write-Behind Pattern
异步更新数据库数据,应用系统只与缓存系统交互
解决方案(Cache-Aside Pattern)
缓存延时双删
延迟时间的目的就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
删除缓存重试机制
重试最好使用异步方式,比如发送消息到 mq 中间件,实现异步解耦。
读取 binlog 异步删除
收藏
0 条评论
下一页