redis设计与实现

数据结构

SDS

在这里插入图片描述

sds相对于C语言中的字符串,多了长度和未使用字节数量。 c语言每次进行append,如果忘记扩大内存,就会溢出,并且获取字符串长度需要O(N)时间,而SDS API在append时会判断是否字符数组够,不够就会扩大, 并且len字段可以直接返回实际字符长度。 buf预留了一些位置便于直接加入元素数据

链表

list底层就是链表

redis中链表是一个双向链表

哈希表(字典)

跟大众的一致。另外,redis有两个hashtable数组,第二个是为了rehash(扩容或者收缩)用,将第二个hashtable的容量设置为第一个的2的N次幂,如果是收缩则设置为使用过得2的n次幂, 迁移数据后,再互换就行

跳跃表

其实就是改进版的列表,每个节点有多个指针,指向后面不同的元素

在这里插入图片描述
具体的结构体如下:
在这里插入图片描述
level的大小一般是在1到32之间随机生成一个树,span表示该当前节点到该前进节点之间的跨度,用来获取rank。
值得一提的是,如果查找倒序,那么就是从尾结点一个一个往后遍历。

压缩列表

列表键和哈希键的底层实现之一,每个节点如下,通过指针运算和previous entry length可以根据当前节点地址计算出前一个节点的地址。 压缩列表相对于数组,长度更小,因为content是不定长的,但是数组是一次性连续分配,每个元素长度是固定的。

在这里插入图片描述

intset

整数集合,当一个集合只包含整数值元素,并且不多时,就会用整数集作为集合键的底层实现。

对象

redis使用对象来表示键值对,然后对象使用到了上述的多种数据结构
键都是redis字符串都想,而值可能是多种对象
在这里插入图片描述

某个键值对,还会动态的转换数据结构

  • 字符串对象,value的数据类型是int, emptySDS和SDS

  • 列表对象, value的数据结构是压缩列表或者链表,而里面可能会嵌套字符串对象

  • 哈希对象, value的数据结构是压缩列表或者hashtable,当元素小于512并且所有键值对长度都小于64字节时用hashtable, 否则用压缩列表

  • 集合对象,使用intset或者hashtable作为value的数据结构
    在这里插入图片描述

  • 有序集合, value的底层数据结构是跳跃表或者压缩列表,当元素数量少于128并且所有元素成员的长度都小于64时是压缩列表,否则是跳跃表。

对象的回收

使用引用计数法,进行回收

对象共享

redis初始化时会创建0到9999的字符串对象,当服务器用到时,就会复用这些对象,而不是新创建。

过期删除策略

一般来讲三种:定时(每个过期键一个定时器),惰性,定期

redis采用惰性和定期一起。 因为定时的话,每个key加定时器,会消耗很大cpu

持久化

一般来说,AOF文件的更新频率一般比RDB文件更高,如果服务器开启了AOF持久化功能,会默认使用AOF还原数据库状态,否则是RDB

RDB

save命令在当前线程生成rdb文件,bgsave, 是开启一个子线程进行初始化。

save命令执行期间会阻塞客户端发送的所有请求命令。

可以设置多个配置,配置多长时间内有多少修改就会保存一次,有个计数器会记录上一次保存的时间戳和距离上一次保存后更新redis数据的次数

rdb文件载入时也会一直阻塞

AOF

AOF是追加redis命令。 AOF的持久化功能主要分为三步

  • 命令追加,每次处理客户端的命令之后,就会将命令追加到aof_buf缓冲区
  • 命令写入和同步, 调用flushAppendOnlyFile函数,判断是否需要将aof_buf里的数据写入和保存到AOF文件中,由配置参数flushAppendOnlyFile函数决定

在这里插入图片描述

如果是always或者everysec, 那么要同步到AOF文件时需要调用fsync和fdatasync函数强制将操作系统缓冲区的数据刷新到文件。

重写

AOF不停追加命令,会造成文件很大,因此需要重写,重写的话,就是从redis数据库中直接读取键值对,将其重写到新的文件中。比如lpush一个key的多次,有多条记录,那么只需要从数据库中查出这个key的值,然后生成一条lpush 所有的元素即可。

因为redis是单线程的,所以会在子线程中进行重写操作,但是这要会有问题:子线程重写时如果遇到父线程原道新的修改操作,那么新AOF文件就会缺失

解决方案是,在重写时加一个AOF重写缓冲区,父线程修改数据时会同时向AOF缓冲区和AOF重写缓冲区一起写数据,当子线程结束重写后,会通知父线程,父线程这时候会暂停接受客户端的请求,然后子线程就会将AOF重写缓冲区中的命令写入到AOF文件,再重命名文件。

缓存线上最常见问题

缓存雪崩: 即大面积缓存同时失效

缓存穿透: 即大量请求不在数据库中的数据

缓存击穿: 即热key突然缓存失效,导致请求到了数据库中。

https://segmentfault.com/a/1190000022029639

事件

redis事件分为文件事件和时间事件。

文件事件一般是网络IO,

在这里插入图片描述

时间事件一般是一些内部定时任务

在这里插入图片描述

事件调度是在一个for循环中,取出离当前时间最近的时间事件,计算时间间隔,阻塞这个时间间隔看是否有文件事件,没有则执行时间事件。

客户端和服务端

在这里插入图片描述

复制

复制一般是指主从服务器之间数据的状态一致性。

  • 全量复制在redis中,从服务器开始初始化时,如果是第一次复制,也就是从服务器没有数据,那么就会调用sync命令进行全量同步,然后主服务器会生成最新的rdb文件,同时用缓冲区记录从现在开始所有的写命令,当主服务器bgslave完成后就向从服务器发送,从服务器接受并载入rdb文件,主服务器再讲缓冲区里的写命令发送给从服务器
  • 部分重同步, 在复制期间,如果主库掉线,从服务器重新连接上住服务器重新开始复制时,没必要重新执行sync全量同步,而是执行psync命令,主从服务器都会有一个复制偏移量,记录当前的复制状态,并且主服务器会有复制积压缓冲区,默认1MB,如果偏移量之后的数据不在缓冲区中会执行全量复制,否则就从偏移量字后的数据开始继续进行同步
  • 命令传播,数据同步完之后,对于主redis的新的写命令,主库都要发送给从库执行

心跳检测

从服务器会每隔一秒钟向主服务器请求replication ack 复制偏移量命令

如果一秒内没收到,主服务器就能知道从服务器不可用,如果收到了,还可以从复制偏移量,看从库是否丢失了数据。

Sentinel(哨兵)

Sentinel是一个特殊的redis服务器,用于监控redis集群中主从服务器的在线状态,对有故障地及时下线,如果是主节点,会将其从库选举节点,然后下线的主服务器如果重新上线了,那么会变为新的主服务器的从库。

Sentinel初始化是和普通redis服务器类似,但是运行的是sentinel代码。

在这里插入图片描述

Sentinel会向主服务器创建命令连接和订阅连接,多个sentinel时会,sentinel之间只会创建命令链接。

  • Sentinel会用map数据结构存储master, slave服务器信息, 其他Sentinel的数据

  • 每隔10秒,sentineal会向监控的主服务器和从服务器发送INFO命令,从而获得主服务器的信息和主服务器对应的从服务器的信息(包括ip, port, role),还会和主从服务器建立订阅连接。

  • 每隔两秒,对于监控同一个主服务器的sentinel来说,他们会每隔1秒通过向服务器的_sentinel_:hello频道发送信息来宣告自己的存在

  • 每隔1s, sentinel会向主服务器,从服务器,其他sentinel发送ping命令,判断是否在线,如果不在线,会进行下线。如果是主服务器,则每个sentinel还会询问其他sentinel是否会下线,判断是否是客观下线,投票通过后就会进行下线

  • 下线操作会选举一个领头sentinel,每个sentinel向其他sentinel发送命令,每个sentinel接受到命令后将最先接受到的sentinel请求端记录,最终判断谁的记录最多,比如A,B,C三个sentinel, B , C都是最先收到A的请求,那么A就是领头sentinel

  • 下线后,如果是主库,会将其从库选举主库,然后下线的主服务器如果重新上线了,那么会变为新的主服务器的从库。 其中选举新的主库,会先判断从库的状态,然后选复制偏移量最新的从库为主库

Sentinel选举主库的原则是,优先级最大、偏移量最大、ID最小

集群

每个节点都会通过clusterNode结构保存自己的信息和集群中其他节点的信息。

集群中的整个数据库被分成了16384个槽,集群中的每个节点可以处理0到之多16384个。当一个集群中,16384个槽都有节点在处理时,节点才是可用的,否则就是不可用的。

每个节点会记录自己处理的槽的记录,也会将slots数组通过消息发送给集群中的其他节点,并且保存其他节点的slots信息。还会记录集群中所有slots信息。

MOVED

当在集群中一个节点中执行命令时,如果key所在的槽不在当前节点上,就会返回MOVED错误,以及槽所在的节点信息。

在这里插入图片描述

重新分片

当集群中间主节点有变更时需要进行重新分片,每个节点负责的槽的信息会有变动。

会迁移槽的信息,并有数据结构保存正在迁移的信息

在这里插入图片描述

ASKING

在重新分片过程中,如果客户端向一个节点发送命令,但是key正处于迁移中,key在目标节点中,那么节点会向客户端返回新的节点的信息,客户端会再次请求新节点执行命令。(集群模式下,ASK并不会报错,会自动转向)

具体流程如下

在这里插入图片描述

在发送ASKING命令之前,客户端命令会带有一次性的REDIS_ASKING标识,正在执行迁移的节点会破例执行一次,下次还需要再带上才行。

故障转移

集群中的每个节点都会定期向其他节点发送ping消息判断其他节点是否下线,如果认为下线,还会向其他节点询问,如果过半都认为下线了,那么就会真正下线掉,将自己的clusternode记录该节点的下线报告,并将该节点标记为下线,还会向其他节点传播该fail消息。

主节点下线后从节点就会有节点被选举为变为主节点

在这里插入图片描述

选举主节点也是raft协议,跟sentinel选举类似

Gossip

集群中的节点发送消息时,是通过gossip协议来传播的,简而言之,就是每个节点传递给某几个节点,然后这几个节点每个也分别扩散到另外的几个节点,最终达到集群所有节点都通知到

主要消息类型如下图,还有一个发布消息

gossip选择节点的机制为:

  • 每秒会随机选择5个节点,找出其中最久没有通信的节点发送ping消息
  • 每100ms都会扫描本地节点列表,如果节点最近一次接受pong的消息时间大于cluster_node_timeout / 2,则理解发送ping消息,避免这个节点长时间不通信

在这里插入图片描述

redis多线程

https://boilingfrog.github.io/2022/05/27/Redis%E4%B8%AD%E7%9A%84%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C(1)-redis%E4%B8%AD%E5%91%BD%E4%BB%A4%E7%9A%84%E5%8E%9F%E5%AD%90%E6%80%A7/#redis-%E5%A6%82%E4%BD%95%E5%BA%94%E5%AF%B9%E5%B9%B6%E5%8F%91%E8%AE%BF%E9%97%AE

redis使用Reactor模型来设计

在这里插入图片描述

redis 6.0之前一直是单线程,原因如下

  • 纯内存,本身开销就小
  • 避免过多上下文切换
  • 避免同步机制造成问题
  • 简单可维护性更强

redis 6.0后使用了多线程,但仅仅是IO处理时使用了多线程,比如可以并发接受请求,并发回复客户端,但是主线程还是单线程修改redis底层数据

redis批量操作

https://www.jianshu.com/p/75137d23ae4a

发布与订阅

redis也通过PUBLISH等命令,具有订阅的功能

有频道订阅和模式订阅。 模式订阅类似于rabbit mq中的模糊匹配。

事务

redis也有事务,如果使用EXEC, WATCH, MULTI等命令

原理

  • 1、事务开始,设置事务状态
  • 2、将命令加入一个队列
  • 3、事务执行,执行里面的命令

WATCH

watch是一个乐观锁,客户端会有,客户端会设置redis_dirty_cas标识,当事务开始后,会将事务要操作的键进行监听,如果有其他客户端进行操作这个键,那么就会拒绝执行。

ACID

redis设计与实现中将到redis事务会满足ACID,但是个人感觉,A不一定能满足,比如事务中有一个执行失败,并不影响其他,那么其实对业务会造成损害。

redis集群架构对比

实际业务中用到的redis架构,一般有两种,redis-cluster, codis

redis cluster

就是普通的redis集群,去中心化,每个节点保存槽信息

客户端对集群进行访问,其实会出现move, ask等重定向操作,访问一个key可能需要两次。

codis

https://juejin.cn/post/7033952057392365604#heading-2

codis是二次开发的redis实例,

在这里插入图片描述

  • codis-server:修改过源码的Redis,支持slot、扩容迁移等。
  • codis-proxy:支持多线程,Go语言实现的内核。
  • Codis Dashboard:集群管理工具。
  • zookeeper, 保存slot信息。

dashboard可以配置slot到codis-server的路由表,然后存在zk和proxy本地,

一般成熟的业务都会采用codis模式,具有一整套方案高可用、数据分片、监控、动态扩态,不方便同意管理。

codis可以看做是一种代理模式,上层应用像是连接到一个单机redis一样。 codis底层会处理请求的转发。

Lua脚本

lua脚本封装的多个redis命令可以当做一个原子命令来实现

可以用于一个秒杀系统。 先查库存,库存小于0则返回,否则库存-1. 如果直接调用,那么会出现并发问题,库存最终可能小于0, 如果使用lua脚本,将上述几个redis命令封装到一起, 那么redis服务器会把它当做一个原子命令(可以类比成一个事务)执行,又是单线程,因此可以解决这个问题。

一般的客户端程序redis集群会处理moved问题吗

一般会自己进行解决,比如jedis,本地维护slot到node的映射。

自己实现的话,没有这个映射表,也可以自己手动重定向,只不过弊端明显,有二次请求

https://cloud.tencent.com/developer/article/1683397

redis锁失效但是任务还没执行完

很多并发接口需要redis锁保证单一执行,但是有的时候接口里逻辑会出现异常超时导致redis锁失效,这时候又来了其他请求,这时候实际上是并发执行了。

有两种方案:

  • 1、 设置大一点redis锁的时间,不过这个不能解决根本问题
  • 2、https://www.51cto.com/article/679902.html 使用watch dog,即当发现锁快要超时但是任务还没执行完,则重新加上锁一段时间。java中有一个redission的框架,原理如下:https://blog.csdn.net/liyanqiang19/article/details/101453297, 其实就是开启一个线程在超时时间之前的一个时间判断线程是否还在,还在就继续。 如果自己去实现,该怎么实现呢? 1、拿到线程id,不停滴循环,看是否还存活

在这里插入图片描述

缓存和数据库数据的一致性

缓存和数据库一般有多种策略

  • 先更新数据库,再更新缓存。并发更新时会有问题,比如由于执行满,先更新的数据,老数据覆盖了最新的数据,最后是脏数据。
  • 先删除缓存,再更新数据库。同时查询和更新时会有问题,过程类似下面。
  • 先更新数据库,再删除缓存。 同时查询和更新时会有问题,查询时缓存刚好失效没查询出来时,去查数据库再更新缓存,这时候有更新操作,数据库更新了,但是删除缓存是在查询的线程中之前,那么缓存中也是有老数据。这种情况概率比较小,因为刚好缓存失效然后这时候又有写操作,并且发生这种异常,加起来概率就会大大降低

上述还有个共性问题,就是删除或者更新缓存失败报错。

其实一般情况下,缓存设置时间就行了,问题不大,如果确实要去精确避免问题。

所以最好使用先更新数据库,再延迟删除缓存。 这样即使发生更新数据库后有其他线程加载老的线程到缓存中,也会通过延迟删除即使纠正掉

redis脑裂

https://cloud.tencent.com/developer/article/1913575
脑裂一般是哨兵认为主库心跳丢失,选取了别的从库作为主库,然而此时主库正在处理一些数据,等这个主库再回头发现已经有别的主库了之后,就全量从新的主库开始同步,所以会有数据丢失
解决办法是

  • 1、设置必须满足一个主库有N个从库
  • 2、设置从库必须再M秒延迟内复制数据后ACK
    这样如果不满足的话,切换完之后,老的主库再接收到请求会拒绝写入

内存淘汰策略

noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
allkeys-lru:在主键空间中,优先移除最近未使用的key。(推荐)
volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
allkeys-random:在主键空间中,随机移除某个key。
volatile-random:在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。

跳跃表和B+树的优劣

https://blog.csdn.net/qq_42259971/article/details/126905577

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值