Redis核心技术与实战
2021-11-18 21:44:16 0 举报
Redis核心技术与实战,Redis学习总结
作者其他创作
大纲/内容
两大维度,三大主线
Redis的问题画像图
键值数据库的基本结构
底层数据结构
时间复杂度
简单动态字符串(simple dynamic string)SDS
SDS的定义
结构体构成
buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。
len:表示 buf 的已用长度。
alloc:表示 buf 的实际分配长度,一般大于 len。
flags:SDS 类型。
SDS与C字符串的区别
常数复杂度获取字符串长度
杜绝缓冲区溢出
减少修改字符串时带来的内存分配次数
空间预分配
惰性空间释放
二进制安全(不仅可以保存文本数据,还可以保存任意格式的二进制数据)
兼容部分C字符串函数库
双向链表
压缩列表
压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度(整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者计算zlend的位置时使用)、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。
prev_len,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。
len:表示自身长度,4 字节;
encoding:表示编码方式,1 字节;
content:保存实际数据。
哈希表
跳表
有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:
可以看到,这个查找过程就是在多级索引上跳来跳去,最后定位到元素。这也正好符合“跳”表的叫法。当数据量很大时,跳表的查找复杂度就是 O(logN)。
整数数组
键和值用什么结构组织?
Redis 使用了一个哈希表来保存所有键值对。哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。
Redis 通过链式哈希的方式解决哈希冲突
rehash
为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
1、给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
2、把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
3、释放哈希表 1 的空间。
到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。
渐进式 rehash
要解决的问题:rehash 的第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。
触发rehash的条件
哈希表的负载因子计算:负载因子 = 哈希表已保存节点数量 / 哈希表大小(load_factor = ht[0].used / ht[0].size)
触发扩容操作条件
服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1。
服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 5。
触发收缩操作条件
当哈希表的负载因子小于 0.1 时,程序自动开始对哈希表执行收缩操作。
实现:简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:
这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
RedisObject
结构体构成
对象的类型和编码
数据类型
String
Redis 使用 jemalloc 内存分配库分配内存,jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。
Redis 的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据。
int 编码:当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。
embstr 编码:当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。
raw 编码:当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。
List
分页的坑
对比新元素插入前后,List 相同位置上的元素就会发生变化,用 LRANGE 读取时,就会读到旧元素
场景
消息队列
排序统计
按插入顺序排序
Hash
Set
聚合统计
交集
SINTER key [key ...] 返回指定所有的集合的成员的交集
SINTERSTORE destination key [key ...] 这个命令与SINTER命令类似,但是它并不是直接返回结果集,而是将结果保存在 destination集合中,如果destination集合存在, 则会被重写
差集
SDIFF key [key ...] 返回一个集合与给定集合的差集的元素
SDIFFSTORE destination key [key ...] 该命令类似于 SDIFF,不同之处在于该命令不返回结果集,而是将结果存放在destination集合中,如果destination已经存在,则将其覆盖重写
并集
SUNION key [key ...] 返回给定的多个集合的并集中的所有成员
SUNIONSTORE destination key [key ...] 该命令作用类似于SUNION命令,不同的是它并不返回结果集,而是将结果存储在destination集合中,如果destination已经存在,则将其覆盖
SortedSet
场景
排序统计
最新列表
排行榜
积分
成绩
ZRANK
ZRANGE
ZRANGEBYSCORE
ZCOUNT
ZSCORE
Bitmap
场景
二值状态统计
GETBIT/SETBIT
BITCOUNT
BITOP
Bitmap 支持用 BITOP 命令对多个 Bitmap 按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中。
HyperLogLog
场景
基数统计
在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。你看,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%
PFADD
PFCOUNT
GEO
场景
位置信息服务(Location-Based Service,LBS)
原理
底层数据结构
SortedSet
GeoHash 编码
对经纬度分别进行:二分区间,区间编码。然后组合编码:组合的规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值
命令
GEOADD
用于把一组经纬度信息和相对应的一个 ID 记录到 GEO 类型集合中
GEOADD cars:locations 116.034579 39.030452 33
GEORADIUS
会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内的其他元素
GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
统计模式下适用的数据类型
IO模型
Redis为什么用单线程?
Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
多线程的开销
我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。为什么会出现这种情况呢?一个关键的瓶颈在于,系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。
并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。而且,采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。
单线程 Redis 为什么那么快?
Redis 大部分操作在内存上完成
高效的数据结构
IO 多路复用机制
持久化
AOF
配置
开启 AOF:appendonly no 改为 appendonly yes
设置写回策略:appendfsync [always/everysec/no]
Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
总结:想要获得高性能,就选择 No 策略;如果想要得到高可靠性保证,就选择 Always 策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择 Everysec 策略。
AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。
写后日志(先执行命令,把数据写入内存,然后才记录日志)
先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。
不会阻塞当前的写操作。
弊端
文件大
恢复慢
两个潜在的风险
如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。
AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
AOF 重写
手动触发:BGREWRITEAOF
自动触发:根据 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数确定自动触发时机(需同时满足)
auto-aof-rewrite-min-size:表示运行 AOF 重写时文件最小体积,默认为 64MB
auto-aof-rewrite-percentage:代表当前 AOF 文件空间 (aof_current_size)和上一次重写后 AOF 文件空间(aof_base_size)的比值,默认为 100%
重写过程 - “一个拷贝,两处日志”
“一个拷贝”:每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存页表(虚拟内存和物理内存的映射索引表)拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把数据写成操作,记入重写日志。
“两处日志”
第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。
第二处日志,就是指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。
RDB
命令
save:在主线程中执行,会阻塞服务器;
bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
配置
save <seconds> <changes> 经过 seconds 秒后且至少 changes 个 key 更改过,则通过执行 bgsave 命令执行 RDB
执行流程图:
弊端
不支持拉链,只有一个 dump.rdb
丢数据相对多一些,宕机后,丢失自上次快照后更新的数据
优点
经过压缩的二进制文件,文件紧凑,相对 aof 文件更小,恢复速度快,直接加载到内存
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。配置:aof-use-rdb-preamble yes
AOF 和 RDB 选择问题的三点建议
数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
如果允许分钟级别的数据丢失,可以只使用 RDB;
如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。
集群
主从
读写分离
通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系
第一次同步
为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。
通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
主从基于长连接的命令传播,可以避免频繁建立连接的开销
主从库间网络断了怎么办?
Redis 2.8 之前 - 全量复制
Redis 2.8 开始 - 增量复制
repl_backlog_buffer 环形缓冲区
当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer。
每个从库一个 replication buffer,所有从库共享一个 repl_backlog_buffer,数据是通过 replication buffer 发送到从库的。
主库会记录自己写到的位置 - master_repl_offset,从库则会记录自己已经读到的位置 - slave_repl_offset。
因为 repl_backlog_buffer 是一个环形缓冲区,所以在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。
参数 repl_backlog_size :配置 repl_backlog_buffer 的大小
建议你留意一下 repl_backlog_size 这个配置参数。如果它配置得过小,在增量复制阶段,可能会导致从库的复制进度赶不上主库,进而导致从库重新进行全量复制。所以,通过调大这个参数,可以减少从库在网络断连时全量复制的风险。
哨兵机制
监控 - 主观下线和客观下线
哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”。
一个哨兵获得了仲裁所需的赞成票数后,就可以标记主库为“客观下线”。这个所需的赞成票数是通过哨兵配置文件中的 quorum 配置项设定的。
误判
集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下
采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。
“客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。这样一来,就可以减少误判的概率,也能避免误判带来的无谓的主从库切换。(当然,有多少个实例做出“主观下线”的判断才可以,可以由 Redis 管理员自行设定)。
选主
首先,判断网络连接状况,down-after-milliseconds * 10,如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。
然后,根据从库优先级、从库复制进度以及从库 ID 号依次进行三轮打分。只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。
第一轮:优先级最高的从库得分高。通过 slave-priority / replica-priority 配置优先级
第二轮:和旧主库同步程度最接近的从库得分高。通过 repl_backlog_buffer 比较各从库 slave_repl_offset 的位置
第三轮:ID 号小的从库得分高。在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。
通知
在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
哨兵集群
sentinel monitor <master-name> <ip> <redis-port> <quorum>
哨兵之间互通机制:基于 pub/sub(发布 / 订阅)机制,主库上有一个名为 “__sentinel__:hello” 的频道,不同哨兵就是通过它来相互发现,实现互相通信的。
哨兵与从库互通机制:哨兵向主库发送 INFO 指令获取所有从库的信息,然后跟从库建立连接,从而监控从库
哨兵判定主库异常机制:哨兵集群中任意一个实例都可以发起主库异常“投票仲裁”流程,向其他哨兵发送 sentinel is-master-down-by-addr 命令
第一,拿到半数以上的赞成票。
第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
切片(分片)集群
Redis应对数据量增多的两种方案
纵向(垂直)扩展(scale up)
优点
实施起来简单、直接
缺点
纵向扩展会受到硬件和成本的限制
在使用 RDB 进行持久化时,Redis 会 fork 子进程来完成,fork 操作的用时和 Redis 的数据量是正相关的,而 fork 在执行时会阻塞主线程。数据量越大,fork 操作造成的主线程阻塞的时间越长。
横向(水平)扩展(scale out)
优点
便于拓展,同时不需要担心单个实例的硬件和成本的限制
缺点
涉及到多个实例的分布式管理问题
节点太多的通信开销
Redis Cluster
哈希槽(hash slot)
16384 hash slots
根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值;然后,再用这个值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
哈希槽与 Redis 实例映射
通过 cluster create 创建集群,Redis 会自动将 16384 个 哈希槽平均分布在集群实例上,比如 N 个节点,每个节点上的哈希槽数 = 16384 / N 个。
手动指定哈希槽,通过 cluster addslots,机器资源配置高的可多分配一些,在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
客户端如何定位数据所在实例
Redis 实例会把自己的哈希槽信息通过 Gossip 协议发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。
客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
重定向机制
reshard(重新分配哈希槽)
在集群中,实例有新增或删除,Redis 需要重新分配哈希槽
负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍
客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其他的实例上。
MOVED 错误
当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。
GET hello:key
(error) MOVED 13320 172.16.19.5:6379
(error) MOVED 13320 172.16.19.5:6379
客户端还会更新本地缓存,将该 slot 与 Redis 实例对应关系更新正确。
ASK 错误
如果某个 slot 的数据比较多,还有部分数据没有迁移。在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息。
GET hello:key
(error) ASK 13320 172.16.19.5:6379
(error) ASK 13320 172.16.19.5:6379
和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。所以,在上图中,如果客户端再次请求 Slot 2 中的数据,它还是会给实例 1 发送请求。这也就是说,ASK 命令的作用只是让客户端能给新实例发送一次请求,而不像 MOVED 命令那样,会更改本地缓存,让后续所有命令都发往新实例。
集群并不能无限增加(Redis 官方给的 Redis Cluster 的规模上线是 1000 个实例。),由于集群通过 Gossip 协议传播集群实例信息,所以通信频率是限制集群大小的主要原因,主要可以通过修改 cluster-node-timeout 调整频率。
hash tag
key被{}括起来的部分用来计算slot
redis 代理
Codis
Twemproxy
Redis性能问题排查和调优
内存碎片
info memory 查看内存使用情况
used_memory_rss 是操作系统实际分配给 Redis 的物理内存空间,里面就包含了碎片。
used_memory 是 Redis 为了保存数据实际申请使用的空间
mem_fragmentation_ratio = used_memory_rss / used_memory
mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的。这是因为,刚才我介绍的那些因素是难以避免的。毕竟,内因的内存分配器是一定要使用的,分配策略都是通用的,不会轻易修改;而外因由 Redis 负载决定,也无法限制。所以,存在内存碎片也是正常的。
mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。
解决方案
如果你使用的是 Redis 4.0 以下版本,只能通过重启实例来解决
如果你使用的是 Redis 4.0 版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理
但是,开启内存碎片整理,它也有可能会导致 Redis 性能下降。原因在于,Redis 的碎片整理工作是也在主线程中执行的,当其进行碎片整理时,必然会消耗 CPU 资源,产生更多的耗时,从而影响到客户端的请求。
Redis 碎片整理的参数配置
内存淘汰策略
noevction(不进行数据淘汰)
进行数据淘汰的策略
在设置了过期时间的数据中进行淘汰
volatile-ttl
volatile-random
volatile-lru
redis 对 lru 算法的优化
背景
LRU 算法在实际实现时,需要用链表管理所有的缓存数据,这会带来额外的空间开销。而且,当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
方案
全局 lru->局部 lru
Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。
Redis 提供了一个配置参数 maxmemory-samples,这个参数就是 Redis 选出的数据个数 N。
当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这儿的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 maxmemory-samples,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。
Redis 提供了一个配置参数 maxmemory-samples,这个参数就是 Redis 选出的数据个数 N。
当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这儿的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 maxmemory-samples,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。
volatile-lfu
思想
LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
方案
在 LRU 策略基础上,把原来 24bit 大小的 lru 字段,又进一步拆分成了两部分
ldt 值:lru 字段的前 16bit,表示数据的访问时间戳
counter 值:lru 字段的后 8bit,表示数据的访问次数
参数
lfu_log_factor
每当数据被访问一次时,首先,用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1
double r = (double)rand()/RAND_MAX;
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
lfu_decay_time
LFU 策略使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值
在所有数据中进行淘汰
allkeys-random
allkeys-lru
allkeys-lfu
事务
常见问题
缓存和数据库的数据不一致问题
https://zhuanlan.zhihu.com/p/380134031
缓存雪崩、击穿、穿透
https://zhuanlan.zhihu.com/p/380214090
原子操作
单命令操作
incr、decr、setnx
lua 脚本
分布式锁
https://zhuanlan.zhihu.com/p/381315478
Redis 主从同步与故障切换的坑
脑裂
定义
在主从集群中,同时有两个主节点,它们都能接收写请求
丢失数据
解决方案
参数
min-replicas-to-write 主库能进行数据同步的最少从库数量
min-replicas-max-lag 主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)
假设从库有 K 个,可以将 min-replicas-to-write 设置为 K/2+1(如果 K 等于 1,就设为 1),将 min-replicas-max-lag 设置为十几秒(例如 10~20s),在这个配置下,如果有一半以上的从库和主库进行的 ACK 消息延迟超过十几秒,我们就禁止主库接收客户端写请求。
Redis 6.0 的新特性
0 条评论
下一页