Redis
2023-06-21 09:41:19 1 举报
AI智能生成
Redis底层原理及部署相关知识
作者其他创作
大纲/内容
高可用
主从复制
主从复制模式
指定主从节点命令
replicaof(Redis 5.0 之前使用 slaveof)
第一次同步
三个阶段
第一阶段是建立链接、协商同步;
psync 命令包含两个参数,分别是主服务器的 runID 和复制进度 offset。
FULLRESYNC 响应命令的意图是采用全量复制的方式,也就是主服务器会把所有的数据都同步给从服务器。
第二阶段是主服务器同步数据给从服务器;
主服务器在下面这三个时间间隙中将收到的写操作命令,写入到 replication buffer 缓冲区里:
第三阶段是主服务器发送新写操作命令给从服务器。
命令传播
第一次同步后,双方之间就会维护一个 TCP 连接
这个连接是长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销。
基于长连接的命令传播(保证第一次后的数据一致性)
分摊主服务器的压力
从节点数量多
主服务器生成 RDB 和传输 RDB 的压力可以分摊到充当经理角色的从服务器。
具体怎么做到
增量复制
网络问题可能导致主从数据不一致
采用增量复制的方式继续同步(Redis 2.8之前采用全量)
三个步骤
从服务器在恢复网络后,会发送 psync 命令给主服务器,此时的 psync 命令里的 offset 参数不是 -1;
主服务器收到该命令后,然后用 CONTINUE 响应命令告诉从服务器接下来采用增量复制的方式同步数据;
然后主服务将主从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。
主服务器怎么知道要将哪些增量数据发送给从服务器呢?
repl_backlog_buffer,一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据
replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,
主服务器使用 master_repl_offset 来记录自己「写」到的位置,
从服务器使用 slave_repl_offset 来记录自己「读」到的位置。
主服务器使用 master_repl_offset 来记录自己「写」到的位置,
从服务器使用 slave_repl_offset 来记录自己「读」到的位置。
选择同步的操作(增量or全量)
如果判断出从服务器要读取的数据还在 repl_backlog_buffer 缓冲区里,那么主服务器将采用增量同步的方式;
相反,如果判断出从服务器要读取的数据已经不存在 repl_backlog_buffer 缓冲区里,那么主服务器将采用全量同步的方式。
repl_backlog_buffer 缓冲区大小
默认大小是 1M,
公式估算
为了应对一些突发的情况,可以将 repl_backlog_buffer 的大小设置为此基础上的 2 倍
配置文件
总结
主从复制共有三种模式:全量复制、基于长连接的命令传播、增量复制。
问题
怎么判断 Redis 某个节点是否正常工作?
互相的 ping-pong 心态检测机制,一半以上的节点去 ping 一个节点的时候没有 pong 回应,
集群就会认为这个节点挂掉了,会断开与这个节点的连接。
集群就会认为这个节点挂掉了,会断开与这个节点的连接。
主从节点发送的心跳间隔,作用的区别
Redis 主节点默认每隔 10 秒对从节点发送 ping 命令
判断从节点的存活性和连接状
判断从节点的存活性和连接状
Redis 从节点每隔 1 秒发送 replconf ack{offset} 命令
上报自身当前的复制偏移量
上报自身当前的复制偏移量
主从复制架构中,过期key如何处理?
主节点模拟一条del命令发送给从节点,从节点收到该命令后,就进行删除key的操作。
Redis 是同步复制还是异步复制?
先写到内部的缓冲区,然后异步发送给从节点
主从复制中两个 Buffer(replication buffer 、repl backlog buffer)有什么区别?
出现的阶段不一样:
这两个 Buffer 都有大小限制的,当缓冲区满了之后,发生的事情不一样:
为什么会出现主从数据不一致?
因为主从节点间的命令复制是异步进行的
如何如何应对主从数据不一致?
第一种方法,尽量保证主从节点间的网络连接状况良好,避免主从节点在不同的机房。
第二种方法,可以开发一个外部程序来监控主从节点间的复制进度。
主从切换如何减少数据丢失?
丢失的情况有两种
异步复制同步丢失
减少异步复制的数据丢失的方案
集群产生脑裂数据丢失
配置文件中有两个参数
(不满足,主节点会禁止写数据)
(不满足,主节点会禁止写数据)
min-slaves-to-write x,有至少 x 个从节点连接。
min-slaves-max-lag x,主从同步延迟不能超过 x 秒。
配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T
主从如何做到故障自动切换?
Redis 哨兵机制
哨兵
(Sentinel)
(Sentinel)
为什么要有哨兵机制?
主从架构中,由于主从模式是读写分离的,如果主节点(master)挂了,那么将没有主节点来服务客户端的写操作请求,也没有主节点给从节点(slave)进行数据同步了。
作用是实现主从节点故障转移
如何工作
监控
每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵
主观下线
如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令
客观下线
只适用于主节点
哨兵集群(最少三台机器部署)
通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。
选主
在哨兵集群中选出一个 leader,让 leader 来执行主从切换
候选者
哪个哨兵节点判断主节点为「客观下线」,这个哨兵节点就是候选者
候选者如何选举成为 Leader
第一,拿到半数以上的赞成票;
第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
注意:投票者只有一票,先请求的候选者就投
通知
主从故障转移的过程
一:选出新主节点
判断从节点之前的网络连接状态
down-after-milliseconds * 10 配置项
三轮考察
第一轮考察:哨兵首先会根据从节点的优先级来进行排序,优先级越小排名越靠前,
第二轮考察:如果优先级相同,则查看复制的下标,哪个从「主节点」接收的复制数据多,哪个就靠前。
第三轮考察:如果优先级和下标都相同,就选择从节点 ID 较小的那个。
选取新主节点后
哨兵 leader 向被选中的从节点 server2 发送 SLAVEOF no one 命令
哨兵 leader 会以每秒一次的频率向被升级的从节点发送 INFO 命令
(没进行故障转移之前,INFO 命令的频率是每十秒一次)
(没进行故障转移之前,INFO 命令的频率是每十秒一次)
二:将从节点指向新主节点
让已下线主节点属下的所有「从节点」指向「新主节点」,
通过向「从节点」发送 SLAVEOF 命令来实现
通过向「从节点」发送 SLAVEOF 命令来实现
所有从节点指向新主节点
三:通知客户的主节点已更换
通过 Redis 的发布者/订阅者机制来实现
哨兵提供的常见消息订阅频道
四:将旧主节点变为从节点
继续监视旧主节点,当旧主节点重新上线时,哨兵集群就会向它发送 SLAVEOF 命令,让它成为新主节点的从节点
组成哨兵集群
配置哨兵的信息
设置主节点名字、主节点的 IP 地址和端口号以及 quorum 值
不需要填其他哨兵节点的信息
主节点上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的。
哨兵 A 把自己的 IP 地址和端口的信息发布到__sentinel__:hello 频道上,哨兵 B 和 C 订阅了该频道。
哨兵 B 和 C 就可以从这个频道直接获取哨兵 A 的 IP 地址和端口号。哨兵 B、C 可以和哨兵 A 建立网络连接。
哨兵 B 和 C 就可以从这个频道直接获取哨兵 A 的 IP 地址和端口号。哨兵 B、C 可以和哨兵 A 建立网络连接。
哨兵获取【从节点】信息
哨兵会每 10 秒一次的频率向主节点发送 INFO 命令来获取所有「从节点」的信息。
总结
作用是实现主从节点故障转移
一般是以集群的方式部署,至少需要 3 个哨兵节点。
哨兵集群主要负责三件事情:监控、选主、通知。
哨兵集群主要负责三件事情:监控、选主、通知。
哨兵节点通过 Redis 的发布者/订阅者机制,组成集群
三轮投票
1、第一轮投票:判断主节点下线
2、第二轮投票:选出哨兵leader
3、由哨兵 leader 进行主从故障转移
Cluster集群(Redis3.0+)
实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题
集群
由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性
不需要sentinel哨兵∙也能完成节点移除和故障转移的功能。
至少需要三个master节点,并且推荐节点数为奇数
(官方推荐不超过1000个节点)【节省机器资源角度
(官方推荐不超过1000个节点)【节省机器资源角度
挂了的结点也算在超过半数的结点
超过半数,不是大于等于
无中心结构
所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
节点的fail是通过集群中超过半数的节点检测失效时才生效
客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
高可用集群
集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
工作机制
在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383
当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了
节点间的通信机制
维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)
两种方式
集中式
元数据的更新和读取,时效性非常好
元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。
常用方式会借助zookeeper集中式存储元数据
gossip协议
ping
pong
响应ping|meet
meet
有新节点加入
fail
有节点宕机
...
槽位定位算法
HASH_SLOT = CRC16(key) mod 16384
跳转重定位
缓存
缓存问题
缓存雪崩
缓存应用过程
导致雪崩的原因
大量数据同时过期;
均匀设置过期时间;
给这些数据的过期时间加上一个随机数,
这样就保证数据不会在同一时间过期。
这样就保证数据不会在同一时间过期。
互斥锁;
保证同一时间内只有一个请求来构建缓存
设置锁的超时时间
双 key 策略;
主 key,会设置过期时间;
备 key,不会设置过期
备 key,不会设置过期
value 值是一样的,相当于给缓存数据做了个副本
同时更新「主 key 」和「备 key 」的数据。
后台更新缓存;
让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
当系统内存紧张的时候,有些缓存数据会被“淘汰”
提前把数据缓起来---缓存预热
Redis 故障宕机;
服务熔断或请求限流机制;
启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误
构建 Redis 缓存高可靠集群;
缓存击穿
某个热点数据过期
两种方案
互斥锁
不给热点数据设置过期时间,由后台异步更新缓存
缓存穿透
当用户访问的数据,既不在缓存中,也不在数据库中
发生一般有这两种情况
业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
常见的方案
第一种方案,非法请求的限制;
第二种方案,缓存空值或者默认值;
第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成
通过 3 个操作完成标记
第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
第三步,将每个哈希值在位图数组的对应位置的值设置为 1;
当有应用要请求,拿N个哈希函数分别做哈希计算,如果都为1,则通行,只要有一个值为0,则说明数据库没有。
布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。
缓存与数据库保持一致性
先后顺序更新,都有并发问题
建议用「先更新数据库,再删除缓存」
有两种方法
采用异步操作缓存。
采用异步操作缓存。
重试机制。(消息队列)
订阅 MySQL binlog,再操作缓存。
数据类型和应用
数据类型与应用
String(字符串)
key-value 结构
value 最多可以容纳的数据长度是 512M
内部实现
底层的数据结构实现主要是 int 和 SDS(简单动态字符串)
SDS 相比于 C 的原生字符串
SDS 不仅可以保存文本数据,还可以保存二进制数据。
SDS 获取字符串长度的时间复杂度是 O(1)
Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。
字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int。
如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstr, embstr编码是专门用于保存短字符串的一种优化编码方式
如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw:
embstr 编码和 raw 编码的边界在 redis 不同版本中是不一样的:
redis 2.+ 是 32 字节
redis 3.0-4.0 是 39 字节
redis 5.0 是 44 字节
embstr编码的字符串对象实际上是只读的,修改值,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令
常用指令
普通字符串的基本操作:
批量操作
计数器(字符串的内容为整数的时候可以使用
过期(默认为永不过期
不存在就插入:
应用场景
缓存对象
直接缓存整个对象的 JSON
采用将 key 进行分离为 user:ID:属性,采用 MSET 存储,用 MGET 获取各属性值
常规计数
因为 Redis 处理命令是单线程,所以执行命令的过程是原子的,
因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
分布式锁
原理
对分布式锁加上过期时间
解锁(两步,需要lua保证原子性)
先判断锁的 unique_value 是否为加锁客户端
将 lock_key 键删除。
共享 Session 信息
Redis 对 Session 信息统一的存储和管理
Hash(哈希)
键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},...{fieldN,valueN}]。Hash 特别适合用于存储对象。
与String区别
内部实现
由压缩列表或哈希表
如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。
Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
结构
listpack 没有记录前一个节点长度,只记录当前节点的长度,从而避免了压缩列表的连锁更新问题。
常用命令
应用场景
缓存对象
Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
子主题
一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
购物车
以用户 id 为 key,商品 id 为 field,商品数量为 value
命令
List(列表)
介绍
简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。
内部实现
双向链表或压缩列表
使用压缩列表
元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置)
每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置)
结构
总结
双向链表
不满足压缩链表就用双向链表
quicklist(3.2后只用)
结构
list-max-ziplist-size 取值范围可以是正数也可以是负数,不能为 0 。当取正数时候代表每个 ziplist 可以存储的最大 entry 数。负数时候去值范围只能是 -5~-1 ,各自代表 ziplist 最大字节大小。
是一个双向链表的复合结构体。quicklistNode 让它拥有链表的查询优点;ziplist 让它在内存使用上有着相对链表可以节省大量前后指针的优势。
需要合理的配置节点中 ziplist 的 entry 个数。entry 过少,则退化成普通链表。entry 过多,则会放大 ziplist 的缺点。
常用命令
应用场景
消息队列
消息保序、处理重复的消息和保证消息可靠性
LPUSH + RPOP (或者反过来,RPUSH+LPOP)命令实现消息队列
1、 如何满足消息保序需求?
生产者使用 LPUSH key value[value...] 将消息插入到队列的头部,如果 key 不存在则会创建一个空的队列再插入消息。
消费者使用 RPOP key 依次读取队列的消息,先进先出。
问题:不停地调用 RPOP 命令(比如使用一个while(1)循环)
解决:BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。
2、如何处理重复的消息?
每个消息都有一个全局的 ID。
消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。
3、如何保证消息可靠性?
消费者获取消息后宕机
留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
总结
消息保序:使用 LPUSH + RPOP;
阻塞读取:使用 BRPOP;
重复消息处理:生产者自行实现全局唯一 ID;
消息的可靠性:使用 BRPOPLPUSH
缺陷
List 不支持多个消费者消费同一条消息
Redis 从 5.0 版本开始提供的 Stream 数据类型了,Stream 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。
Set(集合)
无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
Set 和 List 的区别
List 可以存储重复元素,Set 只能存储非重复元素;
List 是按照元素的先后顺序存储元素的,而 Set 则是无序方式存储元素的。
内部实现
由哈希表或整数集合
如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
常用命令
Set常用操作
Set运算操作
应用场景
风险点
Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。
点赞
key 是文章id,value 是用户id。
共同关注
key 可以是用户id,value 则是已关注的公众号的id。
抽奖活动
key为抽奖活动名,value为员工名称
有去重功能,可以保证同一个用户不会中奖两次
Zset(有序集合)
每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。
Set 类型多了一个排序属性 score(分值)
集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。
内部实现
压缩列表或跳表实现
如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
常用命令
Zset 常用操作
Zset 运算操作(相比于 Set 类型,ZSet 类型没有支持差集运算)
应用场景
可以根据元素的权重来排序,我们可以自己来决定每个元素的权重值。比如说,我们可以根据元素插入 Sorted Set 的时间确定权重值,先插入的元素权重小,后插入的元素权重大。
如果数据更新频繁或者需要分页显示,可以优先考虑使用 Sorted Set。
排行榜
电话、姓名排序
1、电话排序
2、姓名排序
BitMap(2.2 版新增)
Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。
bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
内部实现
用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,
你可以把 Bitmap 看作是一个 bit 数组。
常用命令
bitmap 基本操作
bitmap 运算操作
应用场景
非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有 0 和 1 两种,在记录海量数据时,Bitmap 能够有效地节省内存空间。
签到统计
统计 ID 100 的用户在 2022 年 6 月份的签到情况
如何统计这个月首次打卡时间呢?
Redis 提供了 BITPOS key bitValue [start] [end]指令,返回数据表示 Bitmap 中第一个值为 bitValue 的 offset 位置。
判断用户登陆态
Bitmap 提供了 GETBIT、SETBIT 操作,通过一个偏移值 offset 对 bit 数组的 offset 位置的 bit 位进行读写操作,需要注意的是 offset 从 0 开始。
将用户 ID 作为 offset,在线就设置为 1,下线设置 0
要判断 ID = 10086 的用户的登陆情况
连续签到用户总数
每天的日期作为 Bitmap 的 key,userId 作为 offset,若是打卡则将 offset 位置的 bit 设置成 1。
Redis 提供了 BITOP operation destkey key [key ...]这个指令用于对一个或者多个 key 的 Bitmap 进行位元操作。
假设要统计 3 天连续打卡的用户数,则是将三个 bitmap 进行 AND 操作,并将结果保存到 destmap 中,接着对 destmap 执行 BITCOUNT 统计
即使一天产生一个亿的数据,Bitmap 占用的内存也不大,大约占 12 MB 的内存(10^8/8/1024/1024),7 天的 Bitmap 的内存开销约为 84 MB。同时我们最好给 Bitmap 设置过期时间,让 Redis 删除过期的打卡数据,节省内存。
HyperLogLog(2.8 版新增)
提供不精确的去重计数
优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。
每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
内部实现
常见命令
应用场景
百万级网页 UV 计数
GEO(3.2 版新增)
主要用于存储地理位置信息,并对存储的信息进行操作。
内部实现
用了 Sorted Set 集合类型
使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换
两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。
一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。
常用命令
应用场景
滴滴叫车
Stream(5.0 版新增)
Redis 专门为消息队列设计的数据类型。
Stream 没出来之前,消息队列的实现方式都有着各自的缺陷
用于完美地实现消息队列,
它支持消息的持久化、
支持自动生成全局唯一 ID、
支持 ack 确认消息的模式、
支持消费组模式等,让消息队列更加的稳定和可靠。
常见命令
应用场景
消息队列
Stream 的基础方法(List 也支持)
生产者通过 XADD 命令插入一条消息:
消息的全局唯一 ID 由两部分组成:
消费者通过 XREAD 命令从消息队列中读取消息时,可以指定一个消息 ID,并从这个消息 ID 的下一条消息开始进行读取
(注意是输入消息 ID 的下一条信息开始读取,不是查询输入ID的消息)。
想要实现阻塞读(当没有数据时,阻塞住),可以调用 XRAED 时设定 BLOCK 配置项
XGROUP 创建消费组,使用 XREADGROUP 命令让消费组内的消费者读取消息。
创建两个消费组,这两个消费组消费的消息队列是 mymq,都指定从第一条消息开始读取
消费组 group1 内的消费者 consumer1 从 mymq 消息队列中读取所有消息
再执行一次同样的命令
消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取了,即同一个消费组里的消费者不能消费同一条消息。
用 group2 消费组里的 consumer1 消费者消费消息
不同消费组的消费者可以消费同一条消息
(但是有前提条件,创建消息组的时候,不同消费组指定了相同位置开始读取消息
通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的。
如何保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息?
XACK 命令确认消息已经被消费完成
数据结构
Redis 数据类型的底层数据结构随着版本的更新也有所不同
对象
对象结构设计
type,标识该对象是什么类型的对象(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);
encoding,标识该对象使用了哪种底层的数据结构;
ptr,指向底层数据结构的指针。
对象存储各类型
SDS
C字符串的缺陷
一个字符数组,即数组中每个元素是字符串中的一个字符;
字符数组的结尾位置就用“\0”表示,意思是指字符串的结束。
C 语言获取字符串长度的时间复杂度是 O(N)
字符串的结尾是以 “\0” 字符标识,字符串里面不能包含有 “\0” 字符,因此不能保存二进制数据
字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止
SDS结构设计
O(1)复杂度获取长度
二进制安全
不会发生缓冲区溢出
如果所需的 sds 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍的newlen
如果所需的 sds 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB。
节省空间
5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。
数据结构中的 len 和 alloc 成员变量的数据类型不同。
能灵活保存不同大小的字符串,从而有效节省内存空间
链表list
链表节点结构设计
一个双向链表
链表结构设计
链表的优势与缺陷
优点
listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表;
list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;
缺陷
链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。
保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
压缩列表ziplist
压缩列表结构设计
查找定位第一个元素和最后一个元素,可以通过表头三个字段(zllen)的长度直接定位,复杂度是 O(1)。
查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素
链锁更新
压缩列表的缺陷
空间扩展操作也就是重新分配内存,因此连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。
压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。
哈希表
哈希表结构设计
优点
能以 O(1) 的复杂度快速查询数据
https://cdn.xiaolincoding.com//mysql/other/dc495ffeaa3c3d8cb2e12129b3423118.png
哈希冲突
当有两个以上数量的 kay 被分配到了哈希表中同一个哈希桶上时,此时称这些 key 发生了冲突
Redis 采用了「链式哈希」来解决哈希冲突
链式哈希
随着链表长度的增加,在查询这一位置上的数据的耗时就会增加,毕竟链表的查询的时间复杂度是 O(n)。
rehash
过程
在正常服务请求阶段,插入的数据,都会写入到「哈希表 1」,此时的「哈希表 2 」 并没有被分配空间。
数据增多,触发 rehash ,过程分三步
缺点
第二步迁移数据有问题
渐进式rehash
数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
步骤
给「哈希表 2」 分配空间;
在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
rehash 的过程中
查找
新增
rehash触发条件
当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
跳表zskiplist
跳表的介绍
Zset 对象的底层实现用到了跳表
跳表的优势是能支持平均 O(logN) 复杂度的节点查找
zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表
跳表结构体
跳表的头尾节点,便于在O(1)时间复杂度内访问跳表的头节点和尾节点;
跳表的长度,便于在O(1)时间复杂度获取跳表节点的数量;
跳表的最大层数,便于在O(1)时间复杂度获取跳表中层高最大的那个节点的层数量;
跳表结构设计
Zset 对象要同时保存「元素」和「元素的权重」,对应到跳表节点结构里就是 sds 类型的 ele 变量和 double 类型的 score 变量。
每个跳表节点都有一个后向指针(struct zskiplistNode *backward),指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。
level 数组中的每一个元素代表跳表的一层,也就是由 zskiplistLevel 结构体表示,比如 leve[0] 就表示第一层,leve[1] 就表示第二层。
zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度时用来记录两个节点之间的距离。
跨度实际上是为了计算这个节点在跳表中的排位
跳表节点查询过程
两个判断条件
如果当前节点的权重「小于」要查找的权重时,跳表就会访问该层上的下一个节点。
如果当前节点的权重「等于」要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。
两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。
举例
3 层级的跳表
要查找「元素:abcd,权重:4」的节点
跳表节点层数设置
相邻两层的节点数量的比例会影响跳表的查询性能
跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。
跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
为什么用跳表而不用平衡树?
从内存占用上来比较,跳表比平衡树更灵活一些
在做范围查找的时候,跳表比平衡树操作要简单。
从算法实现难度上来比较,跳表比平衡树要简单得多
整数集合intset
整数集合结构设计
Set 对象的底层实现之一(只有整数且数量少)
如果 encoding 属性值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t 类型的数组,数组中每一个元素的类型都是 int16_t;
如果 encoding 属性值为 INTSET_ENC_INT32,那么 contents 就是一个 int32_t 类型的数组,数组中每一个元素的类型都是 int32_t;
如果 encoding 属性值为 INTSET_ENC_INT64,那么 contents 就是一个 int64_t 类型的数组,数组中每一个元素的类型都是 int64_t;
整数集合的升级操作
不会重新分配一个新类型的数组,而是在原本的数组上扩展空间,然后在将每个元素按间隔类型大小分割
16升32
扩容
3 个类型为 int16_t 的元素
加入一个新元素 65535,这个新元素需要用 int32_t 类型
转换为32,然后倒序放置,再将新值插入
好处是节省内存资源
不支持降级操作
quicklist
quicklist结构设计
「双向链表 + 压缩列表」组合
压缩列表的解决方案
通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
quicklist 会控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,但是这并没有完全解决连锁更新的问题。
quicklistNode 结构体里包含了前一个节点和下一个节点指针,这样每个 quicklistNode 形成了一个双向链表。
但是链表节点的元素不再是单纯保存元素值,而是保存了一个压缩列表,所以 quicklistNode 结构体里有个指向压缩列表的指针 *zl。
添加一个元素
会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,
如果不能容纳,才会新建一个新的 quicklistNode 结构。
listpack
listpack结构设计
采用了压缩列表的很多优秀的设计,比如还是用一块连续的内存空间来紧凑地保存数据,并且为了节省内存的开销,listpack 节点会采用不同的编码方式保存不同大小的数据。
每个 listpack 节点结构
listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
键值对数据库是怎么实现的?
Redis 保存键值对所涉及到的数据结构(Redis 对象和数据结构)
redisDb 结构,表示 Redis 数据库的结构,结构体里存放了指向了 dict 结构的指针;
dict 结构,结构体里存放了 2 个哈希表,正常情况下都是用「哈希表1」,「哈希表2」只有在 rehash 的时候才用,具体什么是 rehash,我在本文的哈希表数据结构会讲;
ditctht 结构,表示哈希表的结构,结构里存放了哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;
dictEntry 结构,表示哈希表节点的结构,结构里存放了 **void * key 和 void * value 指针, key 指向的是 String 对象,而 value 则可以指向 String 对象,也可以指向集合类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象。
持久化
AOF
AOF 持久化是怎么实现的
AOF 日志
只会记录写操作命令,读操作命令是不会被记录的
先执行后写日志
好处
避免额外的检查开销,语法正确才写入日志
不会阻塞当前写操作命令的执行
风险
丢失的风险
可能会给「下一个」命令带来阻塞风险
三种写回策略
写入 AOF 日志的过程
3 种写回硬盘的策略
Always
每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
Everysec
每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
No
Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
都无法能完美解决「主进程阻塞」和「减少数据丢失」的问题
Always 策略
可以最大程度保证数据不丢失
它每执行一条写操作命令就同步将 AOF 内容写回硬盘,影响主进程性能
No 策略
相比于 Always 策略性能较好
AOF 日志内容没有写回硬盘,一旦服务器宕机,就会丢失不定数量的数据。
Everysec 策略
避免了 Always 策略的性能开销,也比 No 策略更能避免数据丢失
可能会丢失上一秒的数据
选择
如果要高性能,就选择 No 策略;
如果要高可靠,就选择 Always 策略;
如果允许数据丢失一点,但又想性能高,就选择 Everysec 策略。
控制 fsync() 函数的调用时机
Always 策略就是每次写入 AOF 文件数据后,就执行 fsync() 函数;
Everysec 策略就会创建一个异步任务来执行 fsync() 函数;
No 策略就是永不执行 fsync() 函数;
AOF 重写机制
为了避免 AOF 文件越写越大
读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
为什么要先写到新的 AOF 文件再覆盖过去。
如果 AOF 重写过程中失败了,现有的 AOF 文件就会造成污染
AOF 后台重写
重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的
两个好处
子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。
子进程是怎么拥有主进程一样的数据副本的呢?
主进程在通过 fork 复制 【页表】给子进程;
两者的虚拟空间不同,但其对应的物理空间是同一个。
写时复制(Copy On Write)
当父进程或子进程在向这个内存发起写操作时,CPU 就会触发写保护中断;
进行物理内存的复制,将父子进程的内存读写权限设置为可读写
在发生写操作的时候,操作系统才会去复制物理内存
有两个阶段会导致阻塞父进程
创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长
创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长
AOF 重写缓冲区
主进程修改了已经存在 key-value,就会发生写时复制,
注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的
重写期间,主进程的工作
执行客户端发来的命令;
将执行后的写命令追加到 「AOF 缓冲区」;
将执行后的写命令追加到 「AOF 重写缓冲区」;
重写完成后
主进程收到该信号后,会调用一个信号处理函数
将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
这个过程也有阻塞主进程的风险
新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
总结
每执行一条写操作命令,就将该命令以追加的方式写入到 AOF 文件,然后在恢复时,以逐一执行命令的方式来进行数据恢复。
三种将 AOF 日志写回硬盘的策略,分别是 Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上则是从低到高。
为了避免日志文件过大,提供了 AOF 重写机制,会fork一个子进程扫描所有数据,每一条数据生成一条命令,写入新的AOF文件,最后覆盖原先的文件。
RDB(快照)【默认开启】
RDB 快照是怎么实现的
记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
对比AOF
在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
丢失记录比AOF多
快照怎么用
两个命令
save
会阻塞主线程,在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长 ;
bgsave
避免主线程的阻塞,会创建一个子进程来生成 RDB 文件
配置文件实现
通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。
执行快照时,数据能被修改吗
执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。
写时复制技术(Copy-On-Write, COW)
执行 bgsave 命令的时候,会通过 fork() 创建子进程,
会复制父进程的页表,但是页表指向的物理内存还是一个
只有在发生修改内存数据的情况时,物理内存才会被复制一份
发生了写时复制后,RDB 快照保存的是原本的内存数据
极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。
RDB 和 AOF 合体(Redis 4.0)
配置成 yes
混合持久化工作在 AOF 日志重写过程
fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件
然后主线程处理的操作命令会被记录在重写缓冲区里
重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件
写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
优点
前半部分是 RDB 内容,这样加载的时候速度会很快。
使得数据更少的丢失
大 Key 对持久化的影响
对 AOF 日志
Always 策略(同步执行fsync()函数,阻塞主线程)
Everysec 策略(异步执行,不会影响主线程)
No 策略,没影响
对 AOF 重写和 RDB(bgsave)
分别都是fork子进程,复制页表
写时复制(大Key的影响)【阻塞主进程】
Linux 开启了内存大页,影响 Redis 的性能(默认是关闭的)
总结
AOF 配置 Always 写回策略,写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,
因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
AOF重写和RDB(bgsave),fork子进程处理,两个阻塞主进程阶段
复制页表,页表数据大
写时复制,是大Key,复制物理内存
大Key的其他影响
操作耗时,响应时间久
网络阻塞
内存分布不均
如何避免大 Key 呢?
设计阶段,就把大 key 拆分成一个一个小 key
定时检查 Redis 是否存在大 key,如果可以删除,用unlink命令(Redis4.0+)
功能
过期删除策略和内存淘汰策略
过期删除策略
如何设置过期时间?
设置 key 过期时间的命令
在设置字符串时,也可以同时对 key 设置过期时间,共有 3 种命令
查看某个 key 剩余的存活时间,可以使用 TTL <key> 命令。
存活时间结果是 -1,表明 key1 永不过期
取消 key 的过期时间,则可以使用 PERSIST <key> 命令
如何判定 key 已过期了?
过期字典(expires dict)
保存了数据库中所有 key 的过期时间
数据结构
过期字典的 key 是一个指针,指向某个键对象;
过期字典的 value 是一个 long long 类型的整数,这个整数保存了 key 的过期时间;
字典实际上是哈希表
查询一个 key
如果不在,则正常读取键值;
如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
过期删除策略有哪些?
三种过期删除策略
定时删除;
在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
优点
可以保证过期 key 会被尽快删除,也就是内存可以被尽快地释放。因此,定时删除对内存是最友好的。
缺点
定时删除策略对 CPU 不友好。
惰性删除;
不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
优点
因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
缺点
如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
定期删除
每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
优点
通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
缺点
内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少
难以确定删除操作执行的时长和频率。
Redis 过期删除策略是什么?
「惰性删除+定期删除」这两种策略配和使用
惰性删除
定期删除
1、这个间隔检查的时间是多长呢?
默认每秒进行 10 次过期检查一次数据库,此配置可通过 Redis 的配置文件 redis.conf 进行配置,配置键为 hz 它的默认值是 hz 10。
注意:每次检查数据库并不是遍历过期字典中的所有 key,而是从数据库中随机抽取一定数量的 key 进行过期检查。
2、随机抽查的数量是多少呢?
从过期字典中随机抽取 20 个 key;
检查这 20 个 key 是否过期,并删除已过期的 key;
判断是否超过流程的时间上限(默认不会超过 25ms)。
如果本轮检查的已过期 key 的数量,超过 5 个(1/4),则重复第一步。
内存淘汰策略
当 Redis 的运行内存已经超过 Redis 设置的最大内存之后,则会使用内存淘汰策略删除符合条件的 key,以此来保障 Redis 高效的运行。
如何设置 Redis 最大运行内存?
配置文件 redis.conf的参数 maxmemory <bytes>
在 64 位操作系统中,maxmemory 的默认值是 0,表示没有内存大小限制
在 32 位操作系统中,maxmemory 的默认值是 3G(32的系统最大只能4G)
Redis 内存淘汰策略有哪些?
1、不进行数据淘汰的策略
noeviction(Redis3.0+,默认的内存淘汰策略) :不淘汰任何数据,写入触发 OOM,可以正常查询删除
2、进行数据淘汰的策略
在设置了过期时间的数据中进行淘汰
volatile-random:随机淘汰设置了过期时间的任意键值;
volatile-ttl:优先淘汰更早过期的键值。
volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
在所有数据范围内进行淘汰
allkeys-random:随机淘汰任意键值;
allkeys-lru:淘汰整个键值中最久未使用的键值;
allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
config get maxmemory-policy 命令查看当前 Redis 的内存淘汰策略
设置内存淘汰策略有两种方法:
方式一:通过“config set maxmemory-policy <策略>”命令设置
式二:通过修改 Redis 配置文件修改,设置“maxmemory-policy <策略>”
LRU 算法和 LFU 算法有什么区别?
LRU(Least Recently Used |最近最少使用)
传统LRU算法
传统 LRU 算法的实现是基于「链表」结构
传统的 LRU 算法存在两个问题
Redis实现LRU
在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
使用随机采样的方式来淘汰数据,随机取 5 个值(此值可配置),淘汰最久没有使用的那个。
优点
不用为所有的数据维护一个大链表,节省了空间占用;
不用在每次数据访问时都移动链表项,提升了缓存的性能;
无法解决缓存污染问题
LFU( Least Frequently Used |最近最不常用)
核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
LFU 算法会记录每个数据的访问次数。
Redis 对象的结构
Redis对象头的 24 bits 的 lru 字段
在 LRU 算法中,lru 字段是用来记录 key 的访问时间戳,
根据 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
根据 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
在 LFU 算法中,lru 字段被分成两段来存储,
高 16bit 存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。
高 16bit 存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。
注意,logc 并不是单纯的访问次数,而是访问频次(访问频率),因为 logc 会随时间推移而衰减的。
Redis 在访问 key 时,logc 的变化
先按照上次访问距离当前的时长,来对 logc 进行衰减;
然后,再按照一定概率增加 logc 的值
redis.conf 提供了两个配置项,用于调整 LFU 算法从而控制 logc 的增长和衰减
0 条评论
下一页