RedisSyncer是一个Redis同步中间件,通过复制协议模拟slave获取源Redis节点数据写入到目标Redis,实现数据同步。项目主要包含以下子项目:redis同步服务引擎redissyncer-serverredissyncer客户端redissyncer-cliredis数据校验工具redissycner-比较基于docker-compse的集成部署方案redissyncer本文主要介绍reidssyncer引擎(redissyncer-服务器)和实现,以及引擎运行的机制。同步过程原来的redis主从模式主要分为两个阶段。第一阶段是同步rdb镜像,也就是全量同步部分;全量同步完成后,进入命令传播模式,每次成功的数据变更操作都会同步到从节点。redissyncer模拟了这种机制,将两部分进行了拆解。它可以执行完整的同步任务,也可以单独执行完整或增量同步。创建socket,发送auth用户密码(6.0新用户)OK,其他错误send->pingreturn:ERRinvalidpasswordpassworderrorNOAUTHAuthenticationrequired.nopasswordsentoperationnotpermittedoperationnotauthorizedPONGpasswordsuccessful功能:检测主从节点之间网络是否可用。检查主从节点当前是否正在接受处理命令。发送slave节点端口信息REPLCONFlistening-port-->OKsuccess-->其他失败发送slave节点IPREPLCONFip-address-->OKSuccess-->其他失败发送EOF能力(capability)REPLCONFcapaeof-->OKSuccess-->Failure功能:是否支持EOF-styleRDB传输,用于无盘拷贝,就是能够解析出RDB文件的EOF流格式。用于无盘拷贝模式。Redis4.0支持两种能力EOF和PSYNC2Redis4.0之前版本只支持EOF能力发送PSYNC2能力REPLCONFcapaPSYNC2-->OK成功-->失败功能:告诉master支持PSYNC2命令,master会忽略capabilities它不支持。PSYNC2表示支持Redis4.0最新的PSYN复制操作。SendPSYNCPSYNC{replid}{offset}-->FULLRESYNC{replid}{offset}完全同步-->CONTINUE部分同步-->-ERR主服务器低于2.8,不支持psync,从服务器需要sendsync-->NOMASTERLINKRetry-->LOADINGRetry-->超过重试机制的阈值停止任务读取PSYNC命令的状态判断是PSYNC的部分同步还是完全同步--->startheartbeatREPLCONFACK心跳检测处于命令传播阶段,默认情况下从服务器会每秒发送一次REPLCONFACK命令。对主从服务器有3个功能:作用:检测主从服务器的网络连接状态;协助实施min-slaves选项;检测命令丢失。REPLCONFGETACK->REPLCONFACKrdb镜像同步完成后,进入commandpropagation,master会继续推送变化的数据给slave。为了保证RedisSyncer有断点续传、数据补偿、断线重连等机制来保证数据同步时的稳定性和可用性,具体机制如下。断点续传机制RedisSyncer的断点续传机制是基于Redis的replid和offset实现的。RedisSyncer有两个版本的断点续传机制v1和v2。v1版本:v1版本数据写入目的redis后,本地持久化offset,这样下次重启会从上次的offset拉取。但是,由于这种方案,写入目标和偏移持久化的操作不是原子操作。如果中间出现中断,会导致数据不一致。比如数据先写入destination成功,然后offset持久化失败再关机或者重启,那么最后断点续传拉取的最后一个offset数据就会不一致。v2版本:在v2版本策略中,RedisSyncer会通过multi和exec将每个pipelinebatch中没有事务的命令进行打包,并在事务末尾插入一个offsetcheckpoint。断点续传时,需要从目标Redis的db库中查找checkpoint,找到对应源节点的最大偏移量,然后根据偏移量续断点。当前v2版本只支持目标为单机Redis的情况。在v2版本中,v2命令事务封装结构v2checkpoint检查点结构:HASHhsetredis-syncer-checkpoint{value}{value}:*{ip}:{port}-runid{replid}*{ip}:{port}-offset{offset}*pointcheckVersion{version}Redis事务机制不支持回滚,如果事务中途出现错误,事务仍会执行,但可以保证一致性,特殊情况除外.在v2机制中,为了防止‘写放大’,会在目标redis的每个逻辑库中写入一个checkpoint,所以在执行断点续传操作时,同步工具会先扫描目标redis的每个逻辑库中的checkpointtarget并选择里面offset最大的checkpoint作为断点续传的参数。数据补偿机制在数据同步过程中,会出现由于网络稳定性或其他因素导致密钥写入失败的情况。为此,redissyncer实现了一套补偿机制来保证源和目的之间的数据一致性。数据补偿的前提是命令编写的幂等性。因此,在RedisSyncer中,一些非幂等的命令,如INCR、INCRBY、INCRBYFLOAT、APPEND、DECR、DECRBY,在写入目标Redis之前,都会被转换为幂等命令。当目标为单机Redis或Proxy时,RedisSyncer通过管道机制将数据写入目标Redis。每一批流水线的提交都会返回一个结果列表。同步工具将验证管道中结果的正确性。如果某些命令写入失败,同步工具会重试与key相关的那批命令。如果重试次数超过指定的阈值,任务将被中止。对于非幂等结构,比如key很大的list,不会进行数据补偿,任务会强制结束,人工处理。断线重连机制在同步过程中可能会因为网络抖动等原因导致同步工具的源端和目标端之间的连接断开。所以需要一个断线重试机制来保证如果在任务同步的时候出现异常断线问题。与源Redis节点与RedisSyncer、RedisSyncer与目标Redis节点的连接存在断线重连机制,两者都有各自的处理机制。Source重连机制SourceRedis与RedisSyncer之间的断线重连机制是通过记录offset来实现的。当由于网络异常等原因导致连接断开时,RedisSyncer会重新尝试与源Redis节点建立连接,并通过当前任务记录的runid、offset等信息,用于拉取断开前的增量数据.重新建立连接成功后,RedisSyncer的同步任务会在不知不觉中继续同步。当断线重连超过指定的重试阈值或者因为offset被刷掉导致数据无法恢复时,RedisSyncer会暂停当前的同步任务,等待人工干预。目标端重连机制RedisSyncer与目标Redis之间的断线重连机制是通过缓存上一批管道命令来实现的。当连接异常断开时,RedisSyncer会重新连接并回放之前的一批写失败。命令。当播放失败或超过连续重试次数时,RedisSyncer会暂停当前同步任务,等待人工干预。命令的链式处理RedisSyncer采用链式策略处理同步数据。如果任何策略失败,密钥将不会被同步。链式策略流程如图所示。每个密钥将通过RedisSyncer中的策略链进行处理。只要一个策略失效,key就不会同步到目标Redis。例如,如果key过期时间计算策略是在full阶段计算的,如果key已经过期,则key会被自动丢弃。策略链中的策略包括类型策略描述DataAnalysisStrategy命令统计分析KeyFilterStrategy命令过滤DbMappingStrategyDb映射TimeCalculationStrategy过期时间计算RdbCommandSendStrategy全量数据写入AofCommandSendStrategy增量数据写入.........任务管理任务启动进程任务停止以及何时清理process任务被主动停止,RedisSyncer会先停止源端Redis端的数据写入,然后进入数据保护状态,保证一小部分可能还在RedisSyncer中还没有写入到目标端的数据可以完整写入目标端,并正确记录并持久化最后写入的一条数据的偏移量,以保证RedisSyncer在断点续传时能够提供正确的偏移量。任务状态TYPEcodedescriptionstatusSTOP0任务停止使用CREATING1创建使用CREATED2创建完成使用RUN3运行状态使用BROKEN5任务异常使用RDBRUNING6全RDB同步过程中使用COMMANDRUNING7增量同步使用FINISH8完成状态使用(用于文件导入)任务异常处理原则如果一个RedisSyncer任务遇到可能导致数据不一致的错误,RedisSyncer会关闭该任务,等待人工干预。RDB跨版本同步实现RDB文件存在向前兼容的问题,即高版本的RDB文件无法导入到低RDB版本的Redis中。对于数据成员没有顺序要求的命令,如:SET、ZSET、HASH命令解析器将它们解析成一个或多个sadd、zadd、hmset等命令进行处理。对于List等命令等对数据成员有顺序要求的命令,如果命令解析器判断为一个大key,将其拆分为多个子命令,此时必须保证按顺序发送到目标REDIS节点REDIS版本之间存在的问题:由于REDIS是向后兼容的(低版本不能兼容高版本的RDB),其RDB文件协议中有一个vesion版本号标识,REDIS导入RDB或者全量同步执行rdbLoad时,它会先检查RDBVERSION是否符合向后兼容,如果不符合,会抛出Can'thandleRDBformatversion错误。syncer跨版本实现机制为了实现RDB数据的全量同步,syncer将处理RDB文件协议的命令分为两种关于RDBVERSION部分REDISRDB文件结构示例开头-----------------------#RDB是一种二进制格式。文件中没有新行或空格。5245444953#MagicString"REDIS"30303037#4位ASCCIIRDB版本号。在本例中,version="0007"=7RDBVERSION字段----------------------------FE00#FE=代码表示数据库选择器。dbnumber=00关于RDBVERSION检查部分的伪代码:defrdbLoad(filename):rio=rioInitWithFile(filename);#Setflag:#a.服务器状态:rdb_loading=1#b.Loading加载时间:loading_start_time=now_time#c.loadingsize:loading_total_bytes=filename.sizestartLoading(rio)#1.检查文件是否为RDB文件(即文件开头的前5个字符是否为“REDIS”)if!checkRDBHeader(rio):redislog("error,WrongsignaturetryingtoloadDBfromfile")return#2.检查当前RDB文件版本是否兼容(向下兼容)if!checkRDBVersion(rio):redislog("error,Can'thandleRDBformatversion")return..........//关于RDB_VERSION的Redis代码检查rdbver=atoi(buf+5);if(rdbver<1||rdbver>RDB_VERSION){rdbCheckError("Can'thandleRDBformatversion%d",rdbver);gotoerr;}当RedisSyncer在RDB同步时遇到Largekeysplit全量同步阶段的LIST、SET、ZSET、HASH等结构,当数据大小超过阈值时,RedisSyncer会将key以迭代器的形式拆分成多个子命令写入到目标库中,防止一些过大的key一次性读入内存导致程序产生oom,提高同步速度。对于没有大key的命令,同步工具会以序列化和反序列化的形式写入目标。附件一RedisRDB协议redisRDBDump文件格式----------------------------#RDB是二进制格式。文件中没有新行或空格。5245444953#MagicString"REDIS"30303037#4位ASCCIIRDB版本号。在这种情况下,version="0007"=7----------------------------FE00#FE=指示数据库选择器的代码。dbnumber=00----------------------------#Key-ValuepairstartsFD$unsignedint#FD表示“过期时间,以秒为单位”.之后,过期时间被读取为一个4字节无符号整数$value-type#1字节标志,指示值的类型-集合、映射、排序集合等$string-encoded-key#键,编码为redis字符串$encoded-value#值。编码取决于$value-type----------------------------FC$unsignedlong#FC表示“过期时间,以毫秒为单位”。之后,过期时间被读取为8字节无符号长$value-type#指示值类型的1字节标志-集合、映射、排序集合等。$string-encoded-key#键,编码为redis字符串$encoded-value#值。编码取决于$value-type----------------------------$value-type#这个键值对没有过期.$value_typeguaranteed!=toFD,FC,FEandFF$string-encoded-key$encoded-value-----------------------------FE$length-encoding#前一个数据库结束,下一个数据库开始。使用长度编码读取的数据库编号。--------------------------...#这个数据库的键值对,附加数据库FF##RDB文件结束指示器8字节校验和##整个文件的CRC64校验和。RDB文件以神奇的字符串“REDIS”开头5245444953#“REDIS”RDB连接版本号接下来的4个字节以rdb格式存储版本号。这4个字节被解释为ascii字符,然后使用字符串到整数的转换将其转换为整数。00000003#Version=3DatabaseSelector一个Redis实例可以有多个数据库。单个字节0xFE标记数据库选择器的开始。在该字节之后,可变长度字段指示数据库编号。有关如何读取此数据库编号的信息,请参阅“长度编码”部分。键值对在数据库选择器之后,文件包含键值对列表。每个键值对有4个部分-1.键过期时间戳。2.指示值类型的单字节标志3.键,编码为Redis字符串。参见《RedisStringEncoding》4.Values根据值类型进行编码。详见《Redis值编码》附录二RedisRESP协议RedisRESP协议RESP协议是在Redis1.2引入的,但在Redis2.0成为与Redis服务器通信的标准方式。是在Redis客户端中实现的协议。RESP实际上是一种序列化协议,支持以下数据类型:简单字符串、错误、整数、批量字符串和数组。RESP在Redis中用作请求-响应协议,其方式如下:客户端将命令作为批量字符串的RESP数组发送到Redis服务器。服务器根据命令实现使用其中一种RESP类型进行回复。在RESP中,一些数据的类型取决于第一个字节:对于简单的字符串,回复的第一个字节是“+”对于错误,回复的第一个字节是“-”对于整数,回复的第一个字节是“:”对于批量字符串,回复的第一个字节是“$”对于数组,回复的第一个字节是“*”RESP能够使用后面指定的批量字符字符串或数组的特殊变体来表示空值。在RESP中,协议的不同部分总是以“\r\n”(CRLF)结束。RESP简单字符串以“+”字符开头,后跟不能包含CR或LF字符(不允许换行)的字符串,并以CRLF(即“\r\n”)结尾。例如:"+OK\r\n"RESPErrors"-Errormessage\r\n"例如:-ERRunknowncommand'foobar'-WRONGTYPEOperationagainstakeyholdingwrongkindofvalueRESPIntegersIntegersisjustaCRLFterminatedstring,代表一个整数,前缀为“:”字节。例如":0\r\n"":1000\r\n"批量字符串用于表示最大长度为512MB的单个二进制安全字符串。批量字符串编码如下:“$”字节后跟构成字符串的字节数(前缀长度),以CRLF结尾。实际的字符串数据。最终CRLF。“foobar”的编码如下:"$6\r\nfoobar\r\n"当字符串为空时"$0\r\n\r\n"BulkStrings也可以用来表示Null的特殊格式values来表示该值不存在。这种特殊格式,长度为-1,没有数据,所以Null表示为:"$-1\r\n"RESP数组格式:一个'*'字符作为第一个字节,然后在数组中作为十进制数的元素数,后跟CRLF。Array的每个元素的附加RESP类型。空数组表示为:“*0\r\n”“foo”和“bar”的数组表示为“*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"["foo",nil,"bar"](数组中的空元素)*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n