Redis 课程笔记
2024-02-21 18:09:11 0 举报
AI智能生成
Redis 课程笔记
作者其他创作
大纲/内容
宕机恢复
AOF日志
写后日志, 记录写成功的命令
由上到下, 数据安全性递减, 性能消耗递减
AOF重写机制
解决日志文件过大,写入新的命令和恢复慢的问题
AOF重写机制, fork出子进程后, 由子进程将内存中的数据 转换为写命令,写入新的AOF日志
主线程 的写操作会记录两份日志, 一份是原有的AOF日志的缓冲区, 一份是在重写的AOF日志缓冲区
主线程 的写操作会记录两份日志, 一份是原有的AOF日志的缓冲区, 一份是在重写的AOF日志缓冲区
主线程写入的是缓冲区, 在AOF重写完成后,缓冲区内的命令会加入到重写后的日志文件
潜在的性能风险
fork子进程会阻塞主线程, 子进程会复制父进程的内存页表以进行相同内存的访问, 这个过程会阻塞父进程, 内存实例越大,阻塞时间越长
fork子进程后, 父线程修改数据时使用写时复制机制, 写时复制需要申请新的内存空间,在处理BigKey时会有阻塞风险
如果机器上开启了 huge page机制(对TLB缓存友好), 会导致写时复制时 被复制的页表太大(比如2M), 也可能会在写入的时候产生阻塞
RDB快照
内存快照, 将内存中数据持久化为二进制文件存放在磁盘
1.save 由主线程创建快照, 阻塞主线程写操作
2. bgsave 有fork出得子进程创建快照
2. bgsave 有fork出得子进程创建快照
恢复速度,文件大小,传输效率上都要比AOF文件更高效
RDB快照频率不宜过快, 磁盘写入速度有限,频率过快可能上一个还没写完,下一个又开始了
频率过慢, 如果出现宕机, 可能丢失数据过多
频率过慢, 如果出现宕机, 可能丢失数据过多
子进程持久化的时候主线程可以支持 读写操作
写时复制示意图
主线程写操作使用 写时复制 机制, 开辟新的内存保存写后的数据
主线程写操作使用 写时复制 机制, 开辟新的内存保存写后的数据
潜在的性能风险
子进程创建快照会占用大量CPU资源, CPU资源不足可能会产生竞争,拖慢主线程
业务场景中写入操作占多数时,子进程RDB快照创建过程中,主线程写时复制可能会需要现有数据一倍的内存
RDB+AOF
redis 4.0加入的新机制
多次RDB快照之间,使用AOF记录写入指令
多次RDB快照之间,使用AOF记录写入指令
多个RDB快照之间不会应该不会有大量的写入, 所以控制了AOF文件的大小
AOF的加入也解决了RDB快照间隙大丢失大量数据的问题
集群
主从模式
多个redis节点,可以组成主从关系,提高读并发
在从节点执行 "replicaof ip port", 将该节点注册为 命令参数指向节点的从节点
主从节点数据同步
1. 从节点 发送命令 psync 给主节点
2. 首次同步,主节点执行RDB备份, 生成RDB文件
3. 主节点发送 RDB给从节点, 从节点根据RDB文件初始化数据
4. 主节点将RDB文件生成后的写命令记录在repl buffer, 在RDB发送后, 通过长链接将这部分命令发送给从实例
2. 首次同步,主节点执行RDB备份, 生成RDB文件
3. 主节点发送 RDB给从节点, 从节点根据RDB文件初始化数据
4. 主节点将RDB文件生成后的写命令记录在repl buffer, 在RDB发送后, 通过长链接将这部分命令发送给从实例
首次主从同步数据 图示
从节点会 记录 主节点的ID(runId), 和数据的一个offset
从节点会 记录 主节点的ID(runId), 和数据的一个offset
主节点buffer
replication_buffer
每一个从节点连接后, 主节点都会开辟一块内存做为replication_buffer
此buffer中的数据会通过长链接发送给 从节点
replication_backlog_buffer
为了保证在主从链接断开后,可以快速找到差异数据而设计的, 减少断连后全量同步的机率
该buffer是一个环形缓冲区, 写满后继续写入会覆盖最开始写入的数据
主从断连后的恢复
主从节点重连后, 从节点 发送psync+offset给主节点, 主节点从backlog_buffer中以offset查找对应的写命令
将offset之后的命令复制到replication_buffer中发送给从节点
将offset之后的命令复制到replication_buffer中发送给从节点
如果backlog_buffer中已经没有该offset对应的命令, 说明已经被覆盖了, 断连时间太长, 此时会执行全量同步
性能瓶颈
一主多从 可能会占用主节点的资源, 可以布置 主-从-从的结构,减少主节点同步的性能损耗
哨兵集群
一些运行在烧饼模式下的redis实例组成的集群, 哨兵集群去中心化
哨兵集群组建
哨兵集群中各节点链接不依赖配置, 依赖 redis的 pub/sub机制
哨兵会通过主库的pub/sub能力, 在"__sentinel__:hello" 频道发布自己的地址信息, 订阅之后也会收到其他哨兵的信息
多台哨兵机器连接到同一主库交换信息后, 将会互相连接组成集群
哨兵与从库的链接
在选主和通知的时候都需要需要与从库链接
哨兵会使用INFO命令从主库获取到 从库列表, 之后会和各个从库建立链接
主从切换
监控
哨兵实例会定期ping 主从库实例, 如果主从库实例没有按时回复, 哨兵实例会将该实例标记为主观下线
为防止因网络阻塞,抖动引起的误判, 哨兵实例会向其他哨兵询问主库是否下线, 足够多哨兵回复Y,该哨兵将主库标记为客观下线
客观下线的赞成票数有哨兵配置文件中的配置决定
选主
哨兵leader选举
哨兵将主库标记为客观下线后, 发送命令给其他哨兵表明自己要进行选主操作, 然后给自己投一票
其他哨兵会给每轮投票中 第一个请求的哨兵回复Y, 之后的回复N
当哨兵在某一轮投票中满足以下两个条件:
1. 获得半数以上的赞成票
2. 票数要大于等于 配置中的哨兵配置文件中的 quorum 值
1. 获得半数以上的赞成票
2. 票数要大于等于 配置中的哨兵配置文件中的 quorum 值
选举可能失败,没有哨兵达成上面的条件, 则在等待一定时间后,再次开始下一轮选举
为了错开 各个哨兵客观下线的时间,在监测时的间隔时间会增加一些随机数, 防止大部分哨兵同一时间 标记客观下线,减少都投票给自己的机率
从库选主
由哨兵leader在从库中,选择一个升级为主库
筛选符合条件的从库
从库当前网络状况可用
从库之前断连的时间和次数符合配置
给筛选后的从库评分
根据配置的从库优先级,优先级高的 评分更高
比较各个从库的repl_offset, 该值越大说明与主库数据越相近,评分更高
上面条件评分一致的,ID号越小的 评分越高
图示
通知
通知依赖Redis的 发布订阅机制
关键的频道, 订阅后可以收到哨兵推送的信息
客户端可以通过 订阅哨兵的上述频道,了解集群选主的情况
扩容
横向扩容-加配置
增加单个redis节点的 内存和CPU配置, 以增强吞吐能力
机器内存越大, fork子进程时复制内存页表的成本也越高, 很可能会导致 阻塞和延迟
纵向扩容-加机器
切片集群-将数据分布到多台机器上,共同提供服务的的集群
数据切片存储需要解决的问题
数据如何分片保存在多台机器上
客户端如何找到自己需要的数据
无中心化集群,在发生迁移、变动时客户端如何找到正确的分片
切片集群有多个解决方案, 官方在3.0版本后增加 Redis Cluster 方案
分片存储
在数据和机器之间设计了一个 槽的概念, 由数据Key 经过CRC16算法得到一个16bit数, 该数对16384取模, 将数据分配在16384个槽位上
使用 cluster create 命令创建 分片集群, redis 会默认将 所有槽位平均分配到各个分片
该设计的优点
槽位的设计解绑了key和分片机器的绑定关系,不需要维护大量的key和分片的映射关系
扩容缩容时重新分配槽到各个分片, 比重新分配整个集群的数据更简单
经过hash后分配到具体槽上的方式,数据分布会更均匀
分片路由
分片集群启动后,集群内的节点会将负责的槽位信息发送给相连的其他实例, 所以集群内的节点有完整的槽位分配表
客户端与实例链接后, 会同步槽与分片对应关系的记录, 客户端缓存该记录以快速知晓数据所在的分片
槽位变动的情况
集群的实例删减、负载均衡等操作会导致槽位重新分配, 集群内节点可感知,客户端感知不到
重定向机制
客户端根据自身缓存的槽信息去请求节点时, 如果数据对应的槽信息已经迁移, 分片节点会返回槽所在的节点信息
MOVE
节点在槽迁移后对请求的客户端返回的命令, 表示对应槽以迁移到 某个节点上
客户端可以修改自己本地的缓存, 后续请求发送到迁移后的节点
客户端可以修改自己本地的缓存, 后续请求发送到迁移后的节点
ASK
在槽数据还没迁移完的情况下返回该命令给客户端,表示槽数据正在迁移,请求数据在另一个节点上
该命令不会导致客户端修改缓存, 下一次请求还会请求原节点
该命令不会导致客户端修改缓存, 下一次请求还会请求原节点
数据结构
数据类型
数据类型对应关系
压缩列表
有标示位标示列表的头尾和列表的长度
O(1)效率获取 首位节点, 中间节点查询效率O(n)
列表中元素紧凑排列,不依赖指针, 所以当想列表中插入或修改数据时, 该位置后面的数据都需要重新排列
跳表
有序列表中, 每隔N个节点取一个节点,连接后形成新的列表, 以此操作形成的多级索引结构
查询效率 O(logN)
扩展数据类型
BitMap
以字符串类型为地层结构的用于统计 二元值类型的数据结构, 每个比特位可以表示一个是或者否的二元值
提供简单的统计功能, 可以做与或非的逻辑运算, 非常节省空间
HyperLogLog
用于统计基数的数据结构, 和sorted set / hash 相比, 只需要很小的内存就可以统计大概的基数
统计的结果不准确。约有 0.81%的误差率
streams
redis 5.0专门为 消息队列设计的结构, 提供消息队列常用的功能
XADD:插入消息,保证有序,可以自动生成全局唯一 ID; 默认id生成方式时间戳+序号
XREAD:用于读取消息,可以按 ID 读取数据;
XREADGROUP:按消费组形式读取消息;
XPENDING 和 XACK:XPENDING 命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,而 XACK 命令用于向消息队列确认消息处理已完成。
XREAD:用于读取消息,可以按 ID 读取数据;
XREADGROUP:按消费组形式读取消息;
XPENDING 和 XACK:XPENDING 命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,而 XACK 命令用于向消息队列确认消息处理已完成。
支持消费组,但是redis在 主从切换等情况还是有可能丢消息的。
内存结构
redisObject
数据是Long型整数时, 直接存放数据
键值对结构
以 key value数据的指针组合的entry 结构 构成 的全局哈希表
entry 由三个 8字节指针构成, 分别指向 key、value、nextEntry
理想情况
O(1)的查询速度, hashKey可以直接找到对应的 key、value指针
hash表可能出现的问题
哈希冲突
- redis哈希冲突, 会将冲突位置的数据修改为链表
- hash查询后,循环链表查找, 大量冲突后, 查询速度会显著下降
ReHash
大量键值重算哈希和复制会阻塞主线程, redis采用 渐进式哈希 分散性能开销
数据量大,哈希桶不够用,哈希冲突数越来越多, ReHash 重新分配哈希桶, 增加可容纳的数据量
- 申请内存分配个 一个更大的 全局哈希表
- 将原哈希表中的数据,重新计算哈希映射并复制到全局哈希表2
- 释放哈希表1 的内存
大量键值重算哈希和复制会阻塞主线程, redis采用 渐进式哈希 分散性能开销
- 在分配了全局哈希表2后, 不直接进行复制, 正常处理请求, 处理请求后将哈希表1中第一个索引位置的数据重算并复制
触发时机
负载因子 = 哈希表中所有 entry 的个数 / 哈希桶容量
5>负载因子 >=1 时, 在没有进行持久化(RDB || AOF)的时候, 会触发 ReHash
负载因子 >= 5, 此时哈希桶已经严重不足, 会直接进行 ReHash操作
Redis后台的定时任务中,包含ReHash的检查
内存模型
基础类型的内存结构
字符串
SDS简单动态字符串
len
表示buf已经使用的长度
alloc
表示buf分配到的长度
buf
实际的数据, 会在数据尾添加 \0 表示结束(占用1B)
压缩列表
压缩列表结构
zlbytes
列表长度
zltail
列表尾的偏移量
zllen
列表中的元素个数
entry
prev_len
表示一个entry的长度,有两种取值方式:1字节或5字节。
1字节表示一个entry小于254字节,255是zlend的默认值,所以不使用。
1字节表示一个entry小于254字节,255是zlend的默认值,所以不使用。
encoding
编码 一字节
len
当前的数据长度 4字节
content
具体数据
zlend
列表尾,表示列表结束
全局哈希表中存储这 dictEntry, dictEntry由三个8字节指针组成,分别指向key、value、nextEntry
redisObject, 有8字节元数据和8字节的数据或指针组成
int编码
数据是一个Long型整数时, 直接存储该数据, 没有额外的指针开销
embstr编码
数据是一个小于44字节的字符串时, 指针和数据在内存空间连续排列, 这样可以减少内存碎片
raw编码
redisObject存放一个指向实际数据地址的指针
图示
缓存淘汰
淘汰策略(内存不足)
在设置过期时间的数据中进行淘汰
volatile-random
在设置了过期时间的数据中随机进行淘汰
volatile-ttl
在设置了过期时间的数据中按照过期时间远近进行删除, 首先删除最先过期的
volatile-lru
根据LRU算法将设置了过期时间的数据排序, 删除不常使用的
volatile-lfu(Redis 4.0 后新增)
根据LFU算法将设置了过期时间的数据排序, 删除不常使用的
在全部数据中进行淘汰
allkeys-lru
allkeys-lfu
allkeys-random
全部数据中随机选择删除的数据
不进行缓存淘汰-noeviction
过期Key删除策略
惰性删除
请求的时候发现key过期了, 在进行删除
内存压力大, 低频访问的key可能会在内存留很久
内存压力大, 低频访问的key可能会在内存留很久
主动删除
每隔一段时间就进行定期删除
定期删除-每隔一段时间就进行一次扫描查询出ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP个数据, 检查是否过期,过期即删除操作, 如果过期key占比大于25%, 会在进行一次取样-检查-删除
近似LRU
LRU算法需要 维护一个所有key的链表, 需要耗费大量的内存空间, 所以 redis使用的LRU算法是一种近似LRU
redis 记录每个数据的最近访问时间(redisObject的lru字段), 选取N个数据比较这部分数据的lru值,选取最不常用的
当再次需要淘汰数据时, 选择数据的标准是: lru值要小于集合中最小的lru值, 当集合到达了 maxmemory-samples 个数时,淘汰lru最小的数据
当再次需要淘汰数据时, 选择数据的标准是: lru值要小于集合中最小的lru值, 当集合到达了 maxmemory-samples 个数时,淘汰lru最小的数据
将全局最优解转换为 局部最优解, 不用维护链表, 减少空间小号
近似LFU算法
LFU算法, 除了最近访问时间还需要维护 数据的访问次数, redis巍峨了节省空间用的LFU算法也是近似版本的
redisObject中的lru字段记录两份数据, 前16bit记录上次访问的时间戳, 后8bit记录该数据的 访问次数(最大值255)
redis 使用一种策略减少 counter数值过快增长: (当前counter值 * 配置项lfu_log_factor) +1, 然后取倒数得到 值P, 再有(0,1) 的随机数 为 值R, 当P>R时, counter计数+1
redis 使用一种策略减少 counter数值过快增长: (当前counter值 * 配置项lfu_log_factor) +1, 然后取倒数得到 值P, 再有(0,1) 的随机数 为 值R, 当P>R时, counter计数+1
配置不同大小时, 对counter 的缩放效果
线程模型
如果主线程发生阻塞即所有请求被阻塞
redis 6.0 增加了多线程的支持
主线程
单线程处理读写操作, 多路复用处理网络IO
子进程
主线程会创建子进程处理一些操作
bgsave命令、bgrewriteaof命令、主从无盘复制
bgsave命令、bgrewriteaof命令、主从无盘复制
子进程处理任务需要复制主线程的内存页表, 可能会阻塞主线程
子线程
在AOF配置为 everySec时, 后台写AOF日志
开启lazy-free机制后, 某些情况和配置下key删除后的 释放内存操作由子线程完成
潜在阻塞点
Redis实例内部操作的阻塞
集合类型O(n) 操作, 比如 查全量数据、集合类型统计操作
BigKey的删除, 删除时需要释放内存, 当释放内存量大时,会导致操作空闲内存链表时时间过长
删除或清空库的操作, 道理同上
同步写磁盘,磁盘写入瓶颈, 比如 开启always(同步写回)的AOF机制
作为从库加载RDB文件, 时间与RDB文件大小正相关
切片集群同步、迁移数据时, 采用同步策略的情况下BigKey可能出现 阻塞
CPU 核和 NUMA 架构的影响(开发应该不用关注)
多核CPU在未经过绑核操作的情况下, 可能会导致 redis进程频繁切换上下文
绑核的情况下, 网络中断程序未绑核, 可能会导致IO之后的中断程序在其他核心执行, 产生 上下文切换
绑核的情况下, 仅绑定了一个逻辑核, 可能会导致子线程子进程和主线程争抢cpu资源, 这个可以通过绑定到一个物理核的多个逻辑核上缓解
文件系统
AOF写盘
AOF设置为always时, 每次写命令同步写入AOF文件, 此时磁盘写入效率会影响主线程响应速度
AOF设置为 everySec时,写磁盘由子线程执行, 如果AOF重写时, 造成子线程写入阻塞, 下一次的AOF写入将会阻塞
如果对于宕机数据丢失不敏感的业务,可以修改配置 no-appendfsync-on-rewrite yes , AOF重写时不调用fsync写入
或者使用 固态硬盘,提高磁盘的操作效率
Redis 关键系统配置
SWAP机制
操作系统一个应对内存不足的机制, 会将部分内存数据换出到磁盘上, 防止出现OOM
当数据换出到磁盘上后, redis主线程的访问时间可能会出现几十倍的增长
排查方式:
查询redis的进程号: redis-cli info | grep process_id
进入目录: cd /proc/5332
执行: cat smaps | egrep '^(Swap|Size)'
size 是使用的内存大小, swap是换出了多少到 磁盘
查询redis的进程号: redis-cli info | grep process_id
进入目录: cd /proc/5332
执行: cat smaps | egrep '^(Swap|Size)'
size 是使用的内存大小, swap是换出了多少到 磁盘
HugePage机制
内存大页机制可以是内存分配粒度有4K 提高到 2M, 会提高redis的内存分配效率, 但是在写时复制机制下,需要复制的数据量变大了, 可能会引起redis的阻塞
Redis内存碎片
redis提供了内存碎片清理工具, 工作时阻塞主线程
Redis缓冲区
输入输出缓冲区溢出(比如bigKey写入读出) 都有可能引起阻塞
集群数据分布不均(数据倾斜)
以下几种情况可能引起集群中数据分布不均
1. bigkey, 某一个key 对应的value过大(长字符串、大集合), 导致涉及该key的操作都比较耗时
2. 运维对分片集群的slot设置不均匀
3. hashTag 导致, 数据分片出现倾斜
1. bigkey, 某一个key 对应的value过大(长字符串、大集合), 导致涉及该key的操作都比较耗时
2. 运维对分片集群的slot设置不均匀
3. hashTag 导致, 数据分片出现倾斜
只读数据可以使用多数据副本的方式 减少请求的分布, 比如 hashTag增加随机前缀,让每个客户端都读自己的一份副本
redis解决方案
存粗key value都是整数类型的数据
使用字符串类型存储一条数据需要占用64字节, 分别是 key-16B、value-16B、dictEntry-32B(实际占24B,)
实际数据大小16B, 但是字符串类型存放时每份数据占内存64B, 可以使用结构更紧凑的底层结构存储,例如 压缩链表结构的哈希表
redis吞吐量降低排查
1. 判断redis是否真的变慢了
查询 redis 的延迟redis-cli --latency -h 127.0.0.1 -p 6379
redis-cli 命令提供了–intrinsic-latency 选项, 输出参数时间内的最大延迟
2. 确认是否是 慢命令拖慢了主线程
通过 Redis 日志,或者是 latency monitor 工具确认是否有 复杂度高的命令
Redis官方提供了各个命令的复杂度 https://redis.io/commands/
主要常见的就是 操作列表类型的数组, keys命令查所有keys, 都需要遍历靠指针链接的全部节点, O(n) 的复杂度
3.是否是key批量过期可能引起的阻塞
key删除策略, 在大量key同时过期时,会产生多次扫描,直到过期key比例低于采样数据的25%
当大量Key过期时, 可能会导致主线程阻塞, redis 4.0以上版本增加异步删除, 会缓解该情况
原子操作
redis是单线程服务器, 当主线程执行一个命令时,其他命令需要排队等待, 所以单个命令天然时原子性的,不会有并发问题
多个命令或包含一定逻辑运算的可以使用lua脚本执行, redis会保证lua脚本执行的原子性
分布式锁
redis 作为公共存储介质, 记录锁状态, 需要保证 并发下 锁状态符合预期
redis单节点
单节点不需要考虑redis集群的主从同步、备份等问题, 只需要保证 原子性和锁的安全性即可
1. 需要设置过期时间,防止加锁的线程出现异常情况没有释放锁
2. 锁数据的 value 最好使用唯一标示,标示线程,防止其他线程释放自己上的锁
3. 如果标示了线程,那释放的时候需要比对该值, 此时为了保证原子性,应该使用Lua脚本删除key
2. 锁数据的 value 最好使用唯一标示,标示线程,防止其他线程释放自己上的锁
3. 如果标示了线程,那释放的时候需要比对该值, 此时为了保证原子性,应该使用Lua脚本删除key
可以使用setNX命令 + DEL命令 来实现锁的设置和删除
set 命令提供 NX PX操作, 可以以原子操作进行 无数据即设置 + 过期时间, SET key value [EX seconds | PX milliseconds] [NX]
set 命令提供 NX PX操作, 可以以原子操作进行 无数据即设置 + 过期时间, SET key value [EX seconds | PX milliseconds] [NX]
redis集群
集群需要考虑多台机器的数据同步,主从同步, 宕机选主后数据是否完整
redis作者提供了 redLock 的分布式锁实现
1. 客户端记录当前时间后, 分别向所有实例发送请求
2. 超过半数节点的上锁请求返回成功 且 耗费时间小于锁的有效时间, 即为上锁成功
3. 释放锁时执行 删除锁数据的 lua脚本
1. 客户端记录当前时间后, 分别向所有实例发送请求
2. 超过半数节点的上锁请求返回成功 且 耗费时间小于锁的有效时间, 即为上锁成功
3. 释放锁时执行 删除锁数据的 lua脚本
0 条评论
下一页