Redis 双主实现

2024-06-02 13:58
文章标签 实现 redis 双主

本文主要是介绍Redis 双主实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

redis双主设计

背景

目前redis仅支持主从复制模式,可以支持在线备份、读写分离等功能,实际应用中通常通过sentinel服务做主从切换的管理,这增加了管理的复杂度和维护成本,基于此360基础架构组联合DBA从redis内部实现了双主功能。

主从复制介绍

redis支持树形的主从异步复制,并具有非阻塞、部分同步等特性,下面简单介绍下其实现原理以及目前redis主从复制模式在故障发生时的数据丢失情况。

实现原理

数据同步

slave开始连接到master时需要同步master已有的数据,具体过程如下图所示,主要有以下几个步骤:

  1. slave向master发送PSYNC命令,将缓存的master的runid和reploff发送给master。

  2. master根据slave发送的reploff判断需要开始同步的数据是否在当前缓冲区(积压空间)中,如果在的话完成和slave的连接建立并向slave发送CONTINUE回复标志,开始发送积压空间中的数据;否则,向slave发送FULLSYNC标志并开启子进程dump当前时刻的快照数据到本地rdb文件。

  3. slave接收psync命令的返回值,并判断是CONTINUE还是FULLSYNC,如果是CONTINUE则完成连接过程准备接收master数据;否则,触发接收master dump的rdb文件事件,等待master发送rdb文件。

  4. master将快照数据dump到本地文件后,向slave发送rdb文件

  5. slave接收完master发送的rdb文件后,清空本地库并重新加载接收的rdb文件,由于这时的数据变化较大,如果开启了aof选项则还需进行aof rewrite操作

  6. 开始传播master积压空间中的新数据。

数据传播

master实例会维护其slave实例列表,当有更改操作发生时,其会通过连接建立时创建的socket向所有slave实例发送操作命令进行数据传播,同时为了防止故障恢复slave重新连接master时每次都进行全量同步,master实例会内部维护一个缓冲区(积压空间)来缓存部分slave命令,master数据传播的具体过程为:

  1. 写入本地库

  2. 写入积压空间

  3. 触发写入slave实例事件,进行异步传播;如果此刻正在进行数据同步操作则将命令写入缓冲区,待同步操作完成后再触发向slave实例写入事件。

数据丢失

redis主从模式并不能保证数据的100%完整,在网络故障、主库宕机等情况下提升slave为master时可能会丢失部分数据,如下图所示,假如在某个时刻master的数据还未完全传播给slave时,master宕机等情况发生并将slave提升为master,这时原来master未传播的一部分数据将丢失。

双主复制设计

前提假设

由于时间、精力等因素,目前我们在进行双主设计时结合如下实际项目需求进行了一些设计折衷,后续我们会继续完善相关设计。

  • 上层保证某个时间点只有一个master在写

  • 故障时允许丢失少量数据

总体设计

为避免额外维护成本,双主模块完全在redis内部实现,双主两个实例各自创建一个socket进行彼此通信;加入双主复制后不会影响原有的主从复制模式,但如下图所示,主从复制实例可以通过我们新增的doublemasterof命令转化为双主复制,双主复制实例也可以通过原有的slaveof命令转换为主从复制实例。

同步策略

redis双主实例网络故障恢复或重启等情况下会进行重新连接以同步彼此数据,主从模式下同步策略很简单,只需要从库同步主库数据即可,而双主模式下我们必须根据一定的策略来选出一个实例作为数据同步的对象,我们考虑到两种具体同步策略:基于数据量和基于时间戳的同步策略。

数据量同步策略

基于数据量的同步策略可以理解为数据量少的实例去同步数据量多的实例,这种同步策略在故障发生时数据已经全部传播到另外一个实例的情况下,故障恢复后可以保证数据完整性,但如下图所示,假如原来操作在双主A上执行,某一时刻双主A上的操作还未同步到双主B上发生了网络故障,上层会切到B上继续写入,当写入了图中红色所示大小的数据后网络故障恢复,这时进行数据同步时就有可能丢失A或B上的部分数据。

时间戳同步策略

对于基于时间戳的同步策略,我们会在redis内部维护双主实例的最近更新时间戳,故障恢复进行数据同步时时间戳较旧的实例会同步时间戳较新的实例;和基于数据量同步策略一样当故障发生时如果数据已经全部传播到另外实例则故障恢复后可以保证数据完整性,否则,如下图所示故障恢复后将丢失双主A实例的部分数据。

我们的选择

根据业务需求,我们需要保证最新写入的数据不会丢失,所以具体实现上我们选择了基于时间戳的同步策略。

同步实现

我们在redis原有的全同步,部分同步的基础上增加了ignore resync策略以实现双主同步,具体实现如下图所示,有以下几个步骤:

  1. 双主实例A链接到另外一个实例B时会通过发送2mastersync命令,该命令会将A实例最后的更新时间戳发送给B

  2. B实例判断A的最后更新时间戳是否比自己新,如果比自己新则直接将A标示为ONLINE并向A发送IGNORE runid reploff信息,A接收到IGNORE信息后记录下runid reploff,即可完成与B的同步;否则,则按照主从同步方式进行全同步或部分同步。

数据传播

对一个双主实例的更改操作,redis内部会通过双主实例建立连接时创建的socket异步传播给另一个双主实例,这里要解决的问题是要避免数据再次传播回来,具体实现上我们通过双主实例的runid进行判断,每个双主实例内部会维护其另外一个实例的runid信息,当有更改命令要执行时,我们会通过runid来判断该命令是否是其双主实例传播过来的,如果是将不再回传。

复制偏移量维护

redis主从实现机制上,当通信模块接收到主库的更改命令时会直接在从库上增加其复制偏移量来记录数据同步的位置,而对于双主实例我们知道为避免数据循环传播,双主实例A传播给双主实例B的命令不会回传过来,那么该如何维护其复制偏移量呢?设计上我们考虑了两种策略:

策略一

如下图所示,双主A向双主B传播一条数据后,B会回复A一个ACK length确认,A接收到确认信息后将自己的复制偏移量增加length。

策略二

如下图所示,双主A再向B传播数据之前自己主动增加复制偏移量,双主B不会向双主A回复确认信息

策略一对比策略二进一步保证了数据的完整性,但同时带来了一定的网络开销,两种策略都不能完全保证复制偏移量再网路故障下的正确性(策略一在ACK丢失的情况下无法保证复制偏移量正确),结合目前的需求为了不影响性能我们选择了策略二。

小结

结合目前项目的需求我们在redis内部实现了双主功能,但是也有一些需要改进的地方,欢迎大家提出意见,后面我们会不断完善,敬请关注!

这篇关于Redis 双主实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1024162

相关文章

golang 对象池sync.Pool的实现

《golang对象池sync.Pool的实现》:本文主要介绍golang对象池sync.Pool的实现,用于缓存和复用临时对象,以减少内存分配和垃圾回收的压力,下面就来介绍一下,感兴趣的可以了解... 目录sync.Pool的用法原理sync.Pool 的使用示例sync.Pool 的使用场景注意sync.

IDEA实现回退提交的git代码(四种常见场景)

《IDEA实现回退提交的git代码(四种常见场景)》:本文主要介绍IDEA实现回退提交的git代码(四种常见场景),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1.已提交commit,还未push到远端(Undo Commit)2.已提交commit并push到

Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)

《KotlinComposeButton实现长按监听并实现动画效果(完整代码)》想要实现长按按钮开始录音,松开发送的功能,因此为了实现这些功能就需要自己写一个Button来解决问题,下面小编给大... 目录Button 实现原理1. Surface 的作用(关键)2. InteractionSource3.

java对接第三方接口的三种实现方式

《java对接第三方接口的三种实现方式》:本文主要介绍java对接第三方接口的三种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录HttpURLConnection调用方法CloseableHttpClient调用RestTemplate调用总结在日常工作

golang中slice扩容的具体实现

《golang中slice扩容的具体实现》Go语言中的切片扩容机制是Go运行时的一个关键部分,它确保切片在动态增加元素时能够高效地管理内存,本文主要介绍了golang中slice扩容的具体实现,感兴趣... 目录1. 切片扩容的触发append 函数的实现2. runtime.growslice 函数gro

golang实现动态路由的项目实践

《golang实现动态路由的项目实践》本文主要介绍了golang实现动态路由项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习... 目录一、动态路由1.结构体(数据库的定义)2.预加载preload3.添加关联的方法一、动态路由1

使用Python实现调用API获取图片存储到本地的方法

《使用Python实现调用API获取图片存储到本地的方法》开发一个自动化工具,用于从JSON数据源中提取图像ID,通过调用指定API获取未经压缩的原始图像文件,并确保下载结果与Postman等工具直接... 目录使用python实现调用API获取图片存储到本地1、项目概述2、核心功能3、环境准备4、代码实现

MySQL数据库实现批量表分区完整示例

《MySQL数据库实现批量表分区完整示例》通俗地讲表分区是将一大表,根据条件分割成若干个小表,:本文主要介绍MySQL数据库实现批量表分区的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考... 目录一、表分区条件二、常规表和分区表的区别三、表分区的创建四、将既有表转换分区表脚本五、批量转换表为分区

Spring Boot 整合 Redis 实现数据缓存案例详解

《SpringBoot整合Redis实现数据缓存案例详解》Springboot缓存,默认使用的是ConcurrentMap的方式来实现的,然而我们在项目中并不会这么使用,本文介绍SpringB... 目录1.添加 Maven 依赖2.配置Redis属性3.创建 redisCacheManager4.使用Sp

redis在spring boot中异常退出的问题解决方案

《redis在springboot中异常退出的问题解决方案》:本文主要介绍redis在springboot中异常退出的问题解决方案,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴... 目录问题:解决 问题根源️ 解决方案1. 异步处理 + 提前ACK(关键步骤)2. 调整Redis消费者组