redis知识点汇总

什么是redis?

redis是一个基于c语言开发的key-value形式的NOSQL数据库,它是开源免费的,为了解决高并发、高扩展和大数据存储等一系列的问题而产生的数据库解决方法。

 

redis为什么那么快?

基于内存的单数线程处理,避免了上下文切换和资源的竞争。

使用了基于I/O多路复用的网络模型。(可了解selector和epoll的区别)

单线程是指核心命令(接受命令,解析命令,执行命令,返回结果)的处理是串行,redis4.0后引入了多线程,额外的线程用于后台处理,例如删除对象。redis6.0中,多线程主要用于网络I/O阶段(接受命令和返回结果)

 

支持的数据类型?

支持的基本数据类型有5种:string,hash,list,set,zset

1.string

二进制安全的,直接存储整数。内容支持字符串,整数和浮点数。编码有int,embstr(embstr编码格式的字符串)和raw三种格式。保存的是整数值,使用int编码会直接保存一个long类型的数值。保存字符串时,通过SDS实现存储,根据字符串长度选择编码(32字节为标准)

描述:

一个键对应一个value 例如:set msg hello

应用场景:

实现缓存:可以将常用的固定请求结果,存储起来,避免每次加载都要重新访问数据库。

计数器:作为实时计数器,实现快速计数和查询的功能,将结果按照特定的时间持久化到数据库中

2.list

有序列表。编码为ziplist或是linkedlist。当字符串大小大于64字节,或键值数量超过512时会转成linkedlist编码。

描述:

一个键对应一个有序列表,可以有重复值 例如:lset msg hello world

应用场景:

存储列表行的数据结构:粉丝列表,好友列表 分页查询:通过lrange读取闭区间的元素 阻塞队列:通过lpush插入数据,通过Brpop消费数据

3.set

无序列表,自动去重。编码为intset和hashtable。当字符串大小大于64字节,或键值数量超过512时会转成hashtable编码。

描述:

一个键对应一个无序列表,无重复值 例如:sadd num 0 1 2

应用场景:

可以查看共同好友:通过取交集

4.hash

编码为ziplist和hashtable。当字符串的长度超过64字节或键值数量超过512时,使用hashtable。当使用hashtable时,会涉及到hash冲突和扩容的问题,发生hash冲突时,直接在后边以节点的形式插入。当条件满足时(执行BGSAVE或是BGREWRITEOF,负载因子大于5,平常大于1),进行渐进式扩容。扩容时会再创建一个表(大于等于used*2的2的n次幂的数值),根据index数值每次更新一个键的值到新表,当索引为-1时则扩容完毕。当服务器空闲的时候也会花费1毫秒去帮助rehash

描述:

一个键对应多个key-value 例如:hset address baidu li qq ma

5.zset

编码为ziplist和skiplist。当字符串的长度超过64字节或键值数量超过128时使用skiplist编码。当使用skiplist编码的时候,底层会使用跳跃表和字典。使用字典查询分值的速度比较快,使用跳跃表可以进行范围型的操作。

描述:

一个键对应多个value-object。value为数字,会以value的从小到大的顺序进行排序。当value相同时按照object进行排序。 例如:zadd address 1 baidu 2 qq

应用场景:

热搜排行榜

 

redis中的过期策略

定时删除,定期删除和惰性删除。

定时删除:为设置过期时间的key添加定时器,使得过期键尽可能快被删除,释放内存,对内存最友好。对cpu时间最不友好,在cpu时间比较紧张的情况下,将cpu时间用在删除和当前任务无关的过期键上,对服务器的响应时间和吞吐量造成影响。

定期删除:每隔一段时间,分多次对服务器的各个数据库进行遍历,从expires字典(设置的过期时间会保存在expires字典中,key为设置的键值,value为过期的时间戳,比较当前时间和过期时间,如果当前时间大,则过期)中随机检查一部分键的过期时间,如果过期就删除。(需要对执行时长和频率进行把控)

惰性删除:在取键的时候才会对键进行过期检查,如果过期就删除。如果过期键不被再次访问的的话,就永远也不会被删除(除非用户手动执行flushdb),造成内存泄漏。

在redis中使用的是惰性删除和定期删除相结合的策略。redis默认每隔100ms对key进行随机抽查。这样仍然有存在过期的key的存在。

 

内存淘汰策略

在redis内存数据集上升到一定大小时,进行内存淘汰策略: noeviction:默认策略,不淘汰任何key,直接返回错误 allkeys-lru:在所有的key中,使用LRU算法淘汰部分key allkeys-lfu:在所有的key中,使用LFU算法淘汰部分key alllkeys-random:在所有的key中淘汰部分key volatile-lru:在设置了过期时间的key中,使用lru算法淘汰部分key volatile-lfu:在设置了过期时间的key中,使用lfu算法淘汰部分key volatile-random:在设置了过期时间的key中,随机淘汰部分key volatile-ttl:在设置了过期时间的key中,挑选ttl短的key淘汰

 

redis中LRU算法的实现

首次淘汰:随机抽样选出最多n(默认为5,配置为10更接近真实lru效果,但是更消耗cpu)个数据放入待淘汰数据池 evictionPoolEntry。 再次淘汰:随机抽样选出最多n个数据,只要数据比数据池中的任意一条数据的lru小,就将数据填充至数据池,如果数据池满,则移除一个lru最大的。 执行淘汰:挑选数据池中lru最小的一条数据进行淘汰

 

redis的持久化机制

RDB、AOF、混合持久化

RDB

通过命令BGSAVE和SAVE触发,生成RDB文件。SAVE命令会阻塞服务器进程,直到RDB文件创建完毕为止。BGSAVE会fork子进程生成,阻塞只会发生在fork子进程的时候,之后服务器 进程可以正常处理请求。

在BGSAVE执行的期间,客户端发送的SAVE和BGSAVE的命令会被服务器拒绝,BEREWRITEOF命令会被延迟到BGSAVE命令执行完毕之后执行。如果BGREWRITEOF命令正在执行,那么BGSAVE命令会被服务器拒绝。

达到触发条件自动执行一次BGSAVE命令

900秒内至少1次 300秒内至少10次 60秒内至少10000次

服务器维持着一个drity计数器和lastsave属性,记录着上次执行BGSAVE或是SAVE后的修改次数和执行时间。每过100ms进行检查一次条件是否满足。

RDB文件是经过压缩的二进制文件,保存了某个时间点的数据集。 适合灾难恢复,在恢复大数据集时的速度要比AOF快。

AOF

保存redis服务器执行的写命令来记录数据库的状态

aof持久化功能分为三个步骤:文件追加,文件写入,文件同步 文件追加:执行的写命令追加到aof缓冲区 文件写入:将aof缓冲区的内容写道内存缓冲区中 文件同步:page cache中的数据落盘

服务器每次结束一个事件循环之前,会调用flushAppendOnlyFile函数,根据服务器配置的appendfsync参数值,考虑是否需要将aof缓冲区中的内容写入和保存到AOF文件中。

appendfsync参数的三个值: always:将aof缓冲区中的内容写入并同步到aof文件 everysec:将aof缓冲区中的内容写入到aof文件中,1秒之后同步到aof文件 no:将aof缓冲区中的内容写入到aof文件中,何时同步由操作系统决定。

随着服务器的运行,AOF文件中的内容会越来越多,体积越来越大。为了解决AOF文件膨胀的原因,redis提供了AOF文件重写的功能。 重写基于数据库中的内容,从数据库中读取键现在的值,然后用一条命令去记录键值对。通过TYPE中记录的类型可以明确向新AOF文件中添加相应的命令。当重写程序在处理列表,哈希表,集合,有序集合这种带有多个元素的键时,为了避免客户端输入缓冲区的溢出,会先检查元素的数量,当超过配置的常量数量(64)时,会使用多条命令来记录键的值。

混合持久化

AOF重写时,将重写这一刻的内存做RDB快照处理,然后将RDB快照内容和增量的AOF修改内存数据的命令在一起,都写入新的AOF文件

 

主从架构

完整重同步:处理初次复制情况。通过让主服务器创建并发送rdb文件,以及向从服务器发送保存在缓存区里面的写命令来同步。 部分重同步:处理断线后重复制情况。当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接受并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。

从服务器接收到客户端发来的salveof命令,判断是否时第一次执行复制,如果是的话,就向主服务器发送PSYNC?-1,请求主服务器执行完整重同步,这时候主服务器会将自己的运行id发送给从服务器,通过bgsave生成最新的rdb快照文件,生成rdb期间接受的写命令保存在缓冲区,rdb文件和缓冲区内容让从服务器一并执行。同步后主从服务器进行命令传播(命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令 “replication ack 偏移量”。即心跳检测),主服务器不仅将写命令发送给所有从服务器还会将命令写入到复制积压缓冲区里。当主从服务器断线后,判断不是第一次执行复制,发送 “PSYNC 运行id 偏移量” 命令,主服务器接受到的运行id和自己的id相比较,不同的话,执行完全重同步,相同的话,判断偏移量在复制积压缓存区是否存在,不存在执行完全重同步,存在则执行偏移量之后在缓存区中的写命令。

 

哨兵模式

保证redis高可用,当主服务器挂了,通过sentinel能够切换到从服务器机型服务,不需要人为的切换。

sentinel中master字典中保存所有被sentinel监视的主服务器,字典键是监视服务器的名字,值是被监视的服务器对应的sentinelRedisInstance结构。一个sentinel可以监视多个主服务器。 sentinelRedisInstance实例可以是主服务器,从服务器或者sentinel的三种类型。

sentinel会创建两条连接。一条是命令连接,这个连接用于向主服务器发送命令,并接受命令回复。一条是订阅连接,这个连接用于订阅主服务器的sentinel:hello频道(通过这个频道获取其余监视这个主服务器的sentinel信息)。 sentinel会以每10秒一次的频率通过命令连接向被监视的主服务器发送info命令。获取到一方面是关于主服务器本身的信息,包括run_id域记录的服务器运行id,以及role域记录的服务器角色。另一方面是关于主服务器属下所有从服务器的信息。

在分析info命令中包含的从服务器信息时,会检查从服务器对应的实例结构是否已经存在于slaves字典中。 1.如果从服务器对应的实例结构已经存在,那么sentinel对从服务器的实例结构进行更新 2.如果从服务器对应的实例结构不存在,那么说明是新发现的从服务器,则会在slaves字典中为这个从服务器新创建一个实例结构。

sentinel对于新创建的从服务器实例,也会建立两个连接。建立后每10秒发送一个info命令,对从服务器的信息进行更新。

sentinel会以接近2秒(函数实现中,如果当前时间大于距离上次发送命令时间>2秒)一次的频率向主从服务器发送命令:

PUBLISH __sentinel__:hello <s_ip> <s_port> <s_uid> <s_epoch> <m_name> <m_ip> <m_port> <m_epoch>

对于订阅这个频道的sentinel能收到这个信息。这样关联这个服务器的sentinel能够互相发现。 如果存在相应实例结构,则需要更新。如果不存在的话则创建一个新的实例,并且创建一个命令连接。即两个sentinel直接相互创建连接。

判断主观下线检测:在默认情况下,sentinel会以每秒1次的频率会向所有与它创建命令连接的实例(主服务器、从服务器、其他sentinel)发送PING命令,并通过实例返回PING命令回复来判断实例是否在线。如果down_after_millisenconds毫秒内,未收到有效恢复,则会修改这个实例,将其进行主观下线。 当sentinel将一个主服务器判断为主观下线后,会向其他的监视这个主服务器的sentinel进行询问,发送命令is-master-down-by-addr,看他们是否也认为他进入到了下线状态。当达到quorum数量的sentinel判断为下线状态,则进入到客观下线状态。

不同的sentinel中配置的下线条件不同,当有sentinel进入到客观下线状态后,就会进行选举领头sentinel,进行故障转移操作。

故障转移操作: 1.在下线的主服务器的从服务器中,挑选一个从服务器,让其升级为主服务器 2.让新下线的主服务器数显的额从服务器复制新的主服务器 3.下线后的主服务器上线后成为新的主服务器的从服务器

挑选主服务器的规则: 将已下线的主服务器的从服务器保存到列表中然后进行过滤。 1.删除处于下线状态的服务器 2.删除最近5秒内没有回复过领头sentinel的info命令的从服务器 3.删除与已下线主服务器连接断开时间超过down-after-milliseconds * 10的从服务器 4.选择优先级较高的从服务器 5.若优先级相同,则选择偏移量较大的 6.若偏移量相同则按照id排序,选出id最小的从服务器

 

集群模式

哨兵模式解决了主从复制不能自动故障转移,达不到高可用的问题,但还是存在难以在线扩容,redis容量受理与单机配置的问题。cluster模式实现了redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。

所有的节点彼此互联,内部使用二进制协议优化传输速度和带宽 节点的fail是通过集群中超过半数的节点检测失效时才生效 客户端与redis节点直连,不需要中间代理层。连接集群中任何一个可用节点即可。

每个节点都保存着一个clusterState结构

typedef struct clusterState{
    //指向当前节点的指针
       clusterNode *myself;
    //集群当前的配置纪元,用于实现故障转移
       uint64_t cuttentEpoch;
    //集群当前的状态,是在线还是下线
       int state;
    //集群中至少处理着一个槽的节点的数量
       int size;
    //集群节点名单(包括myself节点)
       dict *nodes;
    //槽的指派信息
    clusterNode *slot[16384]; 
}

集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于槽中的其中一个,在redis的每个节点上都处理0-16383个插槽(slot)。 当数据库中的16384个槽都有节点在处理的时候,集群就处于上线,如果有任何一个槽没有得到处理,则会处于下线状态。 clusterNode节点中维护着一个二进制数组(长度为16384/8,即2048个字节),在哪个索引上的数值为1,则表示处理哪个槽。便于将自己维护的槽信息发送给其他节点。 当我们存取key的时候,redis会根据CRC16的算法得出一个结果,然后把结果和16383求&,这样每个key都会对应一个编号在0-16383之间的哈希槽,判断当前节点是否是负责处理键所对应槽的节点,如果是就执行命令。如果不是,那么节点就会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令。

故障检测

集群中每个节点都会定期地向集群中的其他节点发送ping消息,检测对方是否在线,如果接受到ping消息的节点没有在规定的时间内,向发送ping消息的节点返回pong消息,那么发送ping消息的节点会将发送pong消息的节点标记为疑似下线(pfail)。如果超过一半的主节点都标记该节点为pfail,那么这个节点被标记为下线(fail),并向集群中广播一条关于主节点fail的消息。

故障转移

当一个从节点发现自己的主节点进行到已下线的状态,那么该从节点会向集群发送一条信息,收到这条消息并且具有投票权的主节点会向这个从节点投票,每个配置纪元,主节点只能投一次票。如果一个从节点得到超过半数具有投票权的主节点的票数,那么他就会成为主节点,否则进行一个新的配置纪元重新投票。 当从节点成为主节点后,会撤销所有已下线主节点的槽指派,并将这些槽指派给自己。新的主节点会向集群中中广播一条消息,让其他节点知道这个节点由从节点变成了主节点,并且接管了原本下线的节点负责的槽。新的主节点开始接受和自己负责处理的槽有关的命令请求,故障转移完成。

 

发布订阅

服务器维护着一个有关于频道和订阅者的字典 当客户端执行命令订阅某个频道的时候,如果频道有其他的订阅者,那么就将客户端添加到订阅者链表末尾。如果任何订阅者,那么就会将频道和订阅者以key-value的形式添加到字典。

退订的话,直接从链表中删除这个客户端,如果链表为空,同时也删除该频道(即频道名作为的key)

发送消息,会发给这个频道下的所有客户端。

 

事务

将多个命令请求打包,然后一次性,按顺序地执行多个命令,并且在事务执行期间,服务器不会中断事务而去执行其他客户端地命令。在事务中的所有命令都执行完毕,然后才会去处理其他客户端的命令请求。

将命令包含在 MULTI 和 EXEC 标签中。 MULTI表示事务的开始,之后将事务中的命令入队,EXEC表示事务执行时,会按顺序执行队列中的命令。

WATCH命令是一个乐观锁,在EXEC命令执行之前,监视任意数量的数据库键,在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改了,如果是,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

当watch一个键的时候,服务器会将客户端保存在watched_keys字典中。该字典的键为被watch命令监视的数据库键,值为一个链表,记录了所有监视相应数据库键的客户端。所有对数据库进行修改的命令执行之后会对watched_keys字典进行遍历查看是否存在字典中,如果存在会被记录,表示该客户端的事务安全性被破坏。将会拒绝该客户端的事务执行。

 

bitmap

使用字符串对象来表示位数组 在相应的偏移量二进制位上设置值,位数组的偏移量从0开始计数

 

缓存雪崩,缓存击穿,缓存穿透

缓存雪崩

大量热点key在同一时间失效,造成数据库瞬时请求量大,压力骤增。

解决方法:

1.给key设置随机的失效时间。在一个固定的过期时间上加一个随机值

缓存击穿

某个热点key失效的时候,有大量的请求打进来,由于缓存失效,会直接请求数据库。

解决办法:

1.让一个请求走数据库,其他的走缓存。加锁,使相同key只能有一个请求走数据库。

2.让热点key永不过期,由定时任务去异步加载数据更新缓存

缓存穿透

访问一个缓存和数据库都不存在的key,会直接打到数据库上。

解决办法:

1.接口校验。在最外层做一层校验:用户鉴权、数据合法性校验等

2.缓存空值。可以将空值写进缓存,设置一个较短的过期时间,根据产品的业务特性来设置时间长短

3.布隆过滤器。先用布隆过滤器做一层过滤,存在再去查询缓存和数据库。

 

布隆过滤器

布隆过滤器中不存储元素,通过一个数组加上几个hash算法占位置来判断元素的存在,因此有误判性,判定不存在的元素一定不存在,判定存在的元素不一定存在。

 

hello 映射到数组中占据索引1,5,9的位置 world 映射到数组中占据索引0,6,10的位置 假设此时存在一个字符串java经过hash后占据索引1,6,10位置 那么在进行判断时,就会误判存在'java'

不支持删除:如果想删除不存在的'java',那么'hello'和'world'也不存在了。 当数组中占用的位置越来越多的时候,误判率就会提升,就必须清空进行重建。

posted @ 2021-10-14 13:11  半个兄弟  阅读(178)  评论(0)    收藏  举报