redisshake go语言部分源码 功能简要介绍

最近本人在看redisshake3.0源码查了很多资料浅浅记录一下 建议结合源码食用 可能有误

目录

main.go

reader.go

strconv.go

writer.go

func.go

client中redis.go

reply.go

key.go

config.go

entry.go

filter.go

log中的func.go

log中init.go

byte.go float.go int.go intset.go

length.go

listpack.go

stucture中的string.go

ziplist.go

types中的hash.go

types中的interface.go

list.go

types中module.go

types中的set.go

types中stream.go

types中string.go

types中zset.go

rdb.go

aof_reader.go

aof_writer.go

reader中interface

psync.go

rdb_reader.go()

scan_reader.go

statistics.go

crc.go系列文件

utils中的file.go

writer中interface.go

redis.go

redis_cluster.go



main.go

 主要有四个部分

第一部分首先判断命令行参数是否符合要求,然后加载过滤器文件(如果有),接着加载配置文件,最后初始化日志。

第二部分首先打印系统和并发参数信息,然后启动pprof服务器;如果metrics端口配置不为0,则初始化metrics,并启动metrics服务器。

第三部分根据配置文件中的Redis节点类型(standalone或cluster),创建对应的Redis写入器。如果节点类型未知,则抛出异常。

第四部分是数据同步的核心部分。首先使用statistics包进行初始化,然后从通道ch中读取输入数据。接着计算命令的参数信息,包括命令名、参数组和参数键。之后,根据过滤器的策略来过滤命令。如果允许同步,则使用Redis写入器将数据同步到目标Redis节点上,否则进行其他处理。最后,将数据同步完成和结束程序的信息记录在日志中。

reader.go

reader.go主要定义了一些Redis响应和读取相关的函数和类型。

 和

 用于获取下一个Redis响应的数据类型,但不将其从缓冲区中读取。如果缓冲区中下一个字节是RespAttr,即响应属性,会调用DiscardNext方法跳过该字节。如果下一个字节是其它类型,就返回其类型。

strconv.go

主要是字节数组和字符串转换以及整数和浮点数解析相关的函数。

 如parseUint用于将字节数组转换成指定进制、指定大小的uint64类型整数。

函数atoi用于将字节数组转换成int类型的整数。

writer.go

定义了一个redis.Writer类型,该类型具有将数据编码成RESP协议的能力。

NewWriter函数创建Writer指针,该指针作为未包装的writer的包装,同时为Writer分配必要的缓冲区。

 WriteArg函数根据v的类型将v的值编码为RESP协议格式。

WriteArgs函数将字节数组转换为RESP协议格式。

Writer中的bytes函数将输入字节转换为RESP协议中的字节字符串

string函数将输入字符串转换为字节并将其传递给bytes。

uint、int和float函数将输入整数或浮点数转换为字符串,并将字符串传递给bytes。

crlf函数将连续的回车换行符写入接口中。

func.go

只有一个encodeargv函数

EncodeArgv 用于对执行 Redis 命令的参数进行编码。有可能方便 Redis 命令发给目标 Redis 服务器。 也有可能是能让 Redis 命令编码成 RDB 格式,并写入到 RDB 文件中。

client中redis.go

 NewRedisClient: 初始化了一个Redis 客户端

DoWithStringReply:将指定的 Redis 命令发送到服务器,返回字符串类型的结果。

Send:将指定的 Redis 命令序列化并发送到服务器。

SendBytes:将未序列化的命令字节发送到服务器。

flush:将写入缓冲区的命令刷新到输出流中。

Receive:从服务器读取回复,并根据具体类型进行反序列化。

BufioReader:返回客户端当前使用的缓冲读取器。

SetBufioReader:替换客户端当前使用的缓冲读取器。

Scan :用于迭代 Redis 数据库中的键,以便在不阻塞数据库的情况下获取所有键。

reply.go

有几个将interface()数据类型转换(据说是Redis 客户端调用 Redis 命令后,返回的数据类型是 interface{})函数

key.go

CalcKeys:接受一个参数 argv,表示 Redis 命令的参数列表。函数首先解析出 Redis 命令的名称,然后根据命令名称查找对应的命令规范 的redisCommands.进行相关的处理。

keyHash:计算key在哪个slots上。

config.go

如字面意义 全是配置

tomlSource 定义了从 Redis Source 中读取数据时需要的配置选项,包括 Redis 版本、Redis 地址、用户名、密码、TLS 开关等。

tomlTarget 定义了向 Redis Target 写入数据时需要的配置选项,包括 Redis 版本、Redis 类型、Redis 地址、用户名、密码、TLS 开关等。

tomlAdvanced 定义了 RedisShake 的其他高级配置选项,包括数据存储目录、CPU 数量、pprof、metrics 端口等。

tomlShakeConfig 定义了 RedisShake 的配置选项,包括源和目标 Redis 实例的配置选项以及高级选项。

init() :定义了默认的 RedisShake 配置。

os.MkdirAll() :进行目录的创建,以及通过 os.Chdir() 设置工作目录。

runtime.GOMAXPROCS() :设置并发执行环境中的 CPU 核心数量。

entry.go

Entry 的定义:Entry 中主要包含了 Redis 命令的各个参数信息和一些统计信息:

Id 是 RedisShake 为每个命令生成的全局唯一的编号。

    IsBase 表示该命令是否来自于 RDB 文件(如果是,则其在 Redis 数据库的初始数据中存  在)。

    DbId 表示该命令在 Redis 数据库中所属的数据库 ID。

    Argv 是该命令的参数列表。

    TimestampMs 表示该命令在写入目标 Redis 数据库时的时间戳。

    CmdName 是该命令的名称。

    Group 表示该命令所属的组。

    Keys 表示该命令操作的键的列表。

    Slots 是 Redis 库所使用的槽位(slot)信息。

    Offset 表示该命令在源 Redis 数据库中的偏移量。

    EncodedSize 表示编码后的命令长度。

NewEntry(): 用于创建 Entry 的实例。

ToString() :将 Entry 格式化输出为字符串。

filter.go

主要是 RedisShake 中的用于命令过滤功能的模块,通过内置的 Lua 脚本实现了对 Redis 命令的过滤。

log中的func.go

Assert: 用于处理条件判断,如果条件为真,则继续执行,如果条件为假,则通过调用 Panicf 函数抛出异常。

函数 Debugf、Infof 和 Warnf 分别记录日志的 Debug、Info、Warn 三个级别的信息。这些函数都接受一个格式化字符串和一些变量作为参数,以类似 fmt.Printf 函数的方式记录日志。

函数 Panicf 和PanicError 用于记录错误,并通过 logger.Panic() 函数抛出异常。在 Panicf 中,日志记录了调用栈的信息。

PanicIfError :封装函数,用于在错误出现时记录并抛出异常。如果参数 err 不为 nil,则 PanicIfError 函数会调用 PanicError 函数抛出异常。

log中init.go

只有init()函数

一个Go语言日志库的初始化函数,该库使用 zerolog 作为底层日志级别库,配置可以从 config 模块中读取。

byte.go float.go int.go intset.go

ReadByte: 从 io.Reader 中读取一个字节并返回该字节的值。

ReadBytes: 从 io.Reader 中读取指定数量的字节并返回一个字节数组。

其他几个go文件同理

length.go

ReadLength:以一个输入的 io.Reader 为参数,并返回一个无符号整数,用于表示字节数组的长度。

readEncodedLength :根据 Redis RDB 文件中字段的编码方式,读取出相应的长度值,并返回。

listpack.go

ReadListpack 函数:用于读取 Listpack 数据结构中的数据,并转化为字符串数组。

readListpackEntry :根据字节码解析 Listpack 中的一个元素,并将其转换成字符串格式。

stucture中的string.go

ReadString:从 io.Reader 中读取一个字符串并返回。在读取字符串之前,先解析编码后的长度(具体实现在 readEncodedLength 函数中),如果长度太大需要特殊处理,则根据编码类型进行相应的处理。

lzfDecompress:LZF 解压缩函数。LZF 是一种快速解压缩算法,它可以将压缩后数据解压缩成原始的数据。该算法的特点是解压速度快,而它的压缩率相对较低。

ziplist.go

readziplist:读取 Redis 数据库中 zipList 类型的数据的函数。zipList 存储了多个元素,每个元素可以是字符串或整数。

readZipListEntry:这是读取一个 zipList 元素的函数。zipList 中每个元素都是按照一定规则进行编码的。首先读取一字节的编码字节,根据字节的高两位判断是字符串类型还是整数类型。如果是字符串类型,则根据不同的编码字节进行长度解析,并使用 ReadBytes 从 io.Reader 中读取字符串内容。如果是整数类型,则根据不同的编码字节使用 ReadInt* 函数读取整数值,并使用 strconv.FormatInt 转换为字符串返回。

types中的hash.go

实现了 Redis 中 hash 类型的解析和重写功能,其中 hash 类型有四种编码方式:rdbTypeHash 、rdbTypeHashZipmap、rdbTypeHashZiplist、rdbTypeHashListpack。

如,readHash 函数读取 rdbTypeHash 编码的数据,从 io.Reader 中读取键值对的数量,然后循环读取每组键值对的数据,并将其存储在 map[string]string 中。

 

Rewrite 函数将 HashObject 中的数据重写为 Redis 的命令,即将 map[string]string 中的键值对序列化,并封装为 Redis 命令返回。

types中的interface.go

ParseObject() :可以根据类型字节类型 byte 和 key 恢复出相应的 RedisObject 对象。

moduleTypeNameByID() :用于将模块类型的 ID 转换为字符串类型的名称。

list.go

LoadFromBuffer():用于从 Redis RDB 文件中解析数据并填充到 ListObject 对象中.

 readList() :用于解析普通链表类型的列表数据,最后将读取元素添加到 ListObject 的 elements 属性。

readQuickList() :用于解析 Redis 快速列表。

readQuickList2() :用于解析带压缩节点容器的快速列表类型的数据

(对于快速列表,每个节点的数据是压缩列表数据,通过调用 structure.ReadZipList() 方法(在ziplist.go中实现)将其解压缩得到每个列表元素。对于带压缩节点容器的快速列表,每个节点的容器类型仅有两种,PLAIN 和 PACKED,通过读取 container 值决定采用哪一种方式解析其中的元素数据。)

types中module.go

主要定义了一个Moduleobject结构体,应该是让用户自己可以自定义这个模块

 因为并没有写完该代码

types中的set.go

主要实现了一个setobject类型

 LoadFromBuffer :用于将 Redis 二进制数据反序列化为一个 SetObject 实例 

readset:读取 Redis Set 类型的元素,并存储在 setobject.elements 切片中。

Rewrite: 则用于将一个 SetObject 实例序列化为 Redis 命令集合。

types中stream.go

首先Redis Stream 数据结构是一个高性能、持久化、有序、可扩展和轻量级的数据结构,用于在 Redis 中存储和处理数据流。(源码有stream数据类型的结构和编码方式,建议看看)

readstream():主要是读取 Redis RDB 文件中 Stream 数据类型的一些与 Stream 相关的元信息,并据此构造出相应的 Redis 命令,以便后续将这些命令发送到 Redis 服务端。

nextInteger() int64:用于逐个解析字符串切片 elements 中的元素,返回下一个 int64 类型的值。

nextString() string:用于逐个解析字符串切片 elements 中的元素,返回下一个 string 类型的值。

rewrite():用于返回一个redis命令数组。

types中string.go

LoadFromBuffer(rd io.Reader, key string, _ byte):从 Redis 数据库中的字节流中读取数据,将其解析为 String 类型。其中 rd 参数为 io.Reader 类型,key 参数为 string 类型,表示该 String 对象的键。第三个参数不被使用,只是为了满足接口的定义需求。

Rewrite() :将 String 类型对象转换为 Redis 命令。

types中zset.go

LoadFromBuffer():使用io.Reader读取数据从Redis RDB文件中,提取相关信息以存储到ZsetObject中。

readZset():专门用于解析当使用常规类型编码序列化排序集时的数据。它读取集合的长度,然后对于每个条目,它读取成员的字符串和浮点数分数,并将其存储在ZSetEntry结构对象中。

readZset2():实质上与readZset相同,但在使用Redis 5+编码序列化排序集时使用,该编码使用双精度浮点数而不是浮点数。

readZsetZiplist():当使用内存数据结构ziplist序列化排序集时,解析数据。该方法读取zip列表并构造一组ZSetEntry对象。它从zip列表中每隔一个元素读取一个成员字符串,并从每个隔一半位置的元素中读取浮点数分数。

rdb.go

 首先定义了一些常量

kFlagFunction2:函数库数据,用于存储 Redis 的内部函数库。

kFlagFunction:老版本函数库数据,用于兼容 Redis 7.0 rc1 和 rc2。

kFlagModuleAux:模块辅助数据,用于存储 Redis 模块的数据。

kFlagIdle:LRU 空闲时间,用于记录键的最后一次访问时间。

kFlagFreq:LFU 访问频次,用于记录键被访问的频率。

kFlagAUX:RDB 辅助字段,用于存储二进制数据。

kFlagResizeDB:哈希表大小提示,用于通知 Redis 哈希表即将扩容。

kFlagExpireMs:过期时间(毫秒),用于记录键的过期时间。

kFlagExpire:过期时间(秒),Redis 4.0 以下版本使用的过期时间格式。

kFlagSelect:DB 编号,用于表示接下来的键属于哪个数据库。

kEOF:RDB 文件结束标志,用于表示 RDB 文件已经读取完毕。

 Loader 结构体:包含了解析 RDB 文件所需的一些信息,包括当前数据库 id,过期时间(毫秒)、LRU 空闲时间和 LFU 访问频次等。

NewLoader():用于创建一个 Loader 结构体实例。

ParseRDB ()是解析 RDB 文件的核心函数。首先通过 os.OpenFile 打开 RDB 文件,然后读取文件的前9个字节,其中前5个字节是 "REDIS" 字符串,后4个字节是 RDB 版本号。如果读取到的前5个字节不是 "REDIS",则表示该文件格式不正确,无法处理。

接着,parseRDBEntry 函数会解析 RDB 文件的每个条目,具体的解析逻辑可以参考 parseRDBEntry 函数的实现。在解析完整个 RDB 文件后,会将 replStreamDbId 字段返回。

最后,为避免 RedisShake 在解析 RDB 文件完成后误判 RDB 数据传输量过多,会通过 os.Stat 函数获取 RDB 文件大小,并强制更新 statistics.Metrics.RdbSendSize 字段。

是redisshake核心代码。

parseRDBEntry():分几段介绍

 

通过 structure.ReadByte 读取一个字节然后解析它的类型。根据不同类型的字节,解析函数将做出不同的操作。

当读取到的字节为常量 kFlagIdle 时,读取下一个长度字节并将其转换为整型,表示条目的空闲时间。

当读取到的字节为常量 kFlagFreq 时,读取下一个字节并将其转换为整型,表示条目的 LFU 访问频次。

当读取到的字节为常量 kFlagAUX 时,读取键值对,并判断键名是否是 repl-stream-db 或 lua。前者表示当前条目所属的数据库 id,后者表示一个 Lua 脚本。如果键名为 repl-stream-db,那么从键值中读取对应数据库 id,并将其赋值给当前 Loader 实例的 replStreamDbId 字段。这里我们可以看到,repl-stream-db 是 RedisShake 为了支持 Redis 多个数据库而扩展的 RDB 类型。如果键名为 lua,则将其值作为参数创建一个新的 entry,然后将其发送给相应的 channel,这意味着 RedisShake 可以在加载 RDB 的同时预加载 Lua 脚本。

其他情况下,会将读取到的键值对打印出来。

其中defer 语句中的 UpdateRDBSentSize 函数用于定期更新当前 RDB 文件已经传输的大小,便于 RedisShake 控制传输量。

 kFlagResizeDB 表示Redis数据库已调整大小,并且程序记录数据库和过期列表的新大小。 kFlagExpireMs 和 kFlagExpire 表示该键具有过期时间,程序计算到期时间的剩余时间。 kFlagSelect 更改当前选定的数据库。最后, kEOF 表示到达了RDB文件结尾,函数返回。

 

default 分支中,处理所有其他类型的 RDB 条目。首先,函数使用 structure.ReadString(rd) 读取键的名称。然后,函数使用 io.TeeReader() 方法读取键的值,并将值写入一个名为 valuebytes.Buffer 变量中。接下来,函数使用 types.ParseObject() 方法分析值对象。当键值对的长度小于等于最大 Redis 命令长度限制时,代码创建一个新的 entry,其中包含键、值和到期时间,并通过 ld.ch 通道。如果 Redis 服务器支持并在配置文件中(看版本) 提供了 ld.idle 和 ld.freq 字段,将过期时间(ld.expireMs)、空闲时间(ld.idle)和频率(ld.freq)重置为 0,代码会将它们加入到 entry 中。如果长度大于最大 Redis 命令长度限制,则将对象分解为多个命令,并将每个命令作为单独的 entry 写入 Redis 服务器。如果键具有过期时间,代码还会计算到期时间的剩余时间,并将其作为 PEXPIRE 命令和相应的参数写入 Redis 服务器。

在处理完所有的 RDB 条目后,函数使用 select 语句检查是否有一个名为 tick 的通道已准备好发送数据。如果准备好,则通过 UpdateRDBSentSize() 更新发送的 RDB 字节数并进行处理。

aof_reader.go

redisshake的aof读取写的没有rdb精细

....

 openFile():用于打开指定偏移量对应的Redis AOF文件

fmt.Sprintf 方法生成 Redis AOF 文件名,并将其保存到 filename 成员变量中。接着,使用 os.OpenFile 方法打开该文件,并将文件指针保存到 file 成员变量中。其中的 os.O_RDONLY 指定以只读模式打开文件,0644 是文件的权限标志。如果打开文件失败,则调用 log.PanicError 方法记录错误信息并终止程序。

注意这里是直接读文件名 相对于rdb要读开头的魔数来确定版本来说 有点不够精细

 

readNextFile():判断下一个 Redis AOF 文件是否存在,如果存在,则关闭当前文件,并打开下一个文件;否则,不做任何操作。

 read():

定义了 AOFReader 结构体的 Read 方法,用于从打开的 Redis AOF 文件中读取数据到缓冲区 buf 中。

首先,通过调用 r.file.Read 方法读取数据,将读取的字节数保存到 n 变量中,读取的错误信息保存到 err 变量中。如果读取到文件末尾,则进入循环,该循环会一直尝试读取直到读取成功或者出现错误。

在循环内部,首先判断当前打开的文件与偏移量对应的文件名是否一致,如果不一致,则调用 r.readNextFile 方法切换到下一个文件继续读取。接着,调用 time.Sleep 暂停 10 毫秒,等待新文件准备好。最后,调用 r.file.Seek(0, 1) 方法将文件指针移动到当前位置,以便从新文件的开头读取数据。

读取新文件的数据和旧文件的处理方式一样,将字节数保存到 n 变量中,错误信息保存到 err 变量中。如果读取出错,则调用 log.PanicError 方法记录错误信息并终止程序。否则,更新 offset 和 pos 成员变量,返回读取的字节数和 nil 错误信息。

该函数首先调用 file.Read 方法读取 Redis AOF 文件,并保存读取的字节数到 n 变量中。如果读取遇到文件尾,则调用 readNextFile 方法读取下一个文件。调用 time.Sleep 方法阻塞 10 毫秒,避免过于频繁地尝试读取。最后,更新 offset 和 pos 成员变量,返回读取的字节数和 nil。

Offset() :

该函数返回当前文件偏移量 offset。

Close():

调用 file.Close 方法关闭 Redis AOF 文件,并将当前文件指针 file 置为 nil,避免重复关闭。如果关闭失败,则会调用 log.PanicError 方法记录错误信息并终止程序。

aof_writer.go

 

 NewAOFWriter() :用于创建新的 AOFWriter 实例,表示从指定的 offset(由 Redis 分片 ID 加上运行时间戳组成)开始写入 AOF 文件。在该方法中调用 openFile 方法打开文件。

 openFile() :用于打开文件并将文件对象赋值给 file 成员变量。如果打开文件失败,则调用 log.PanicError 方法记录错误信息并终止程序。该方法还会初始化 offset、filename 和 filesize 成员变量。

同样是只看文件名

write() :用于将数据写入 AOF 文件。这里首先调用 file.Write 方法将数据写入文件,如果写入失败,则调用 log.PanicError 方法记录错误信息并终止程序。然后,更新 offset 和 filesize 成员变量,以及检查当前文件是否达到了最大尺寸限制,如果文件大小超过 MaxFileSize,则调用 Close 方法关闭文件,并调用 openFile 方法重新打开一个新文件继续写入。最后,调用 file.Sync 方法将数据刷新到磁盘。

 Close() :用于关闭文件并刷新数据到磁盘。它首先调用 file.Sync 方法将数据刷新到磁盘,并检查错误信息;然后,调用 file.Close 方法关闭文件,并检查错误信息;最后,记录日志信息。

reader中interface

 只有短短的几行

psync.go

 

client: 实例化的 Redis 客户端对象。

address: Redis 服务器的地址。

ch: 读取 Redis 数据后将其存储的通道。

DbId: 需要读取 Redis 数据的数据库编号。

rd: Redis 客户端对象的 bufio.Reader。

receivedOffset: Redis 服务器发送过来的数据偏移量。

elastiCachePSync: 是否使用 AWS ElastiCache PSYNC 解析模式。

 clearDir():

使用 ioutil.ReadDir() 读取当前目录(./)下所有文件。

遍历每个文件,如果其文件名后缀是 .rdb 或者 .aof,则删除该文件。

如果删除文件出现错误,记录错误信息并输出日志。

 saveRDB():

使用 replconf 命令设置监听端口为 10007。

执行 sync 命令,用于开始提供 RDB 文件。

从 Redis 服务中读取 RDB 数据,并将其发送到 r.ch 通道中去。

如果读取出错或者发送数据失败则启动 panic() 抛出报错信息.

 

拼装 PSYNC 命令,并将其发送到 Redis 服务器中。

处理 Redis 服务器返回的 PSYNC 响应数据:

先读取回复前所有的 \n。因为响应数据前面可能会包含多个 \n 字符。这些 \n 字符并不是结构化的数据,而是 Redis 发送 PSYNC 命令时,遗留在网络之中的换行符。

然后读取第一个非 \n、非 -、非 + 的字符。如果是 -,说明在处理 PSYNC 命令时发生了错误,需要立即抛出 panic()。

如果是 +,说明 PSYNC 命令处理成功了,需要停止读取响应数据。

 

 

 

读取 Redis 服务器返回的 master offset 值。该值表示从哪个偏移量开始进行增量同步,后续可以借助该值进行增量同步。

标识当前源端正在进行 bgsave(background save)操作,并统计相关指标信息。

读取 Redis 服务器返回的 RDB 文件长度,从而可以知道需要接收的数据总长度。

标识当前源端在进行 bgsave 操作的时间,并接收 Redis 发送的 RDB 文件数据。

接收 Redis 发送的 RDB 文件数据,直到接收到全部数据为止。

将接收到的 RDB 数据保存到一个本地文件中(例如 dump.rdb)。

rdb_reader.go()

 定义了 rdbReader 结构体,包含文件路径和一个用于传递数据的通道 ch.

NewRDBReader():Reader 接口的实现函数,它接收文件路径并返回一个 Reader。在函数内部,通过 filepath.Abs 将文件路径转换为绝对路径,初始化 rdbReader,并返回一个 Reader 接口,这个接口可以用于获取数据通道。

 startread():

它通过打开文件、计算文件大小并调用 rdbLoader.ParseRDB 方法从 RDB 文件中读取数据,并将数据写入到传递数据通道 ch 中,最后关闭通道。

使用了 os.Stat 函数和文件大小计算。在读取 RDB 文件时,它将读取到内存并且没有办法监视进度,所以在程序运行时,需要使用依赖包中的 statistics.Metrics 记录已经读取的文件大小和总文件大小的信息,以便在运行日志中实时输出 RDB 加载进度。

scan_reader.go

 scanReader 的类型,用于在 Redis 集群环境下从主节点中读取所有 Key.

NewScanReader():

创建一个 scanReader 对象。

分别使用 NewRedisClient 方法创建两个 Redis 客户端 clientScan 和 clientDump,它们分别用于执行 SCAN 和 DUMP 命令。

使用 IsCluster 方法检查 Redis 服务器是否启用了集群模式。

返回 scanReader 对应的通道以便后续模块可以从中读取数据。

 scan():负责执行 SCAN 命令并将结果写入管道 innerChannel 中。

检查 Redis 服务器是否处于集群模式。如果是,只扫描第 0 号数据库;否则遍历扫描 0~15 号数据库。

如果 Redis 不处于集群模式,则先通过 SELECT 命令选择对应数据库。同时,将 innerChannel 中添加一个 dbKey 对象,表示一个数据库。

对每一个数据库进行 SCAN 命令的遍历。每次 SCAN 命令都返回一个游标和一定数量的 Key。遍历过程中将这些 Key 逐一添加到管道 innerChannel 中。同时,为了后续同步需要,将这些 Key 对应的值以及过期时间也添加到 Redis 客户端的管道中。

每次 SCAN 命令的执行结果会被记录在统计信息中,例如扫描到了多少个 Key,当前的游标等信息。

当 SCAN 命令遍历完成以后,关闭 innerChannel 通道,表示所有 Key 的扫描已经完成。

 

 该方法负责从管道 innerChannel 中读取 Key 并通过 Redis 客户端获取它们的值和过期时间,并将其封装成 entry.Entry 对象并发送到管道 ch 中.

遍历 innerChannel,读取其中的 dbKey 对象,表示扫描到了一个数据库。这个时候需要调用 Redis 的 SELECT 命令进行对应的数据库切换。

遍历 innerChannel,读取其中的数据库中的 Key。对于每一个 Key,先通过 Redis 的 DUMP 命令获取其值,并通过 PTTL 命令获取其过期时间。将这些值和过期时间封装成一个 entry.Entry 对象,并发送到管道 ch 中。

如果 Key 在 Redis 中不存在,则忽略它,继续遍历下一个 Key。

每个 entry.Entry 对象都有一个唯一的 Id,通过一个变量 id 进行自增,确保每个对象都有一个不同的 Id。

遍历完成后,通过日志打印提示信息,关闭 ch 通道,表示所有 entry.Entry 对象已经发送完成。

statistics.go

 

 

该代码定义了一个 metrics 结构体,用于记录 Redis 同步服务(kratos)的各种指标。其中包括以下字段:

Address:kratos 运行的地址。

EntryId:最近一条 entry 的 id。

AllowEntriesCount:已读取的允许同步的 entry 的数量。

DisallowEntriesCount:已读取的未允许同步的 entry 的数量。

IsDoingBgsave:是否正在进行 bgsave。

RdbFileSize:RDB 文件大小。

RdbReceivedSize:已经接收到的 RDB 数据的大小。

RdbSendSize:已经发送出去的 RDB 数据的大小。

AofReceivedOffset:已经接收到的 AOF 数据的 offset。

AofAppliedOffset:已经应用的 AOF 数据的 offset。

InQueueEntriesCount:当前还在等待处理的 entry 的数量。

UnansweredBytesCount:当前尚未处理的二进制数据的数量。

ScanDbId:正在扫描的数据库的 id。

ScanCursor:正在扫描的游标。

Msg:记录 kratos 的消息。

Hander():将 metrics 结构体以 JSON 格式输出到 HTTP 响应中。

init():

 

启动了一个 goroutine,负责定时记录 Redis 同步服务(kratos)的状态,并输出到日志。具体来说,该函数会将 kratos 的日志间隔读取周期 seconds(单位:秒)设置到 config.Config.Advanced.LogInterval 变量中,如果该变量小于等于0,则认为 kratos 不需要记录日志。然后进入无限循环,通过判断 kratos 运行类型 config.Config.Type 是否为 "scan",分别记录 kratos 扫描同步和异步同步的状态,并将这些记录信息转化为字符串保存在 Metrics.Msg 中,并输出到日志中。

其中,kratos 扫描同步状态的输出信息包含以下字段:

dbId:当前正在同步的数据库的 id。

percent:扫描进度百分比,计算方法为已经扫描过的游标值 / 最大游标值 x 100%。

allowOps:每秒允许同步的操作数。

disallowOps:每秒拒绝同步的操作数。

entryId:同步的 entry 的 id。

InQueueEntriesCount:当前还在等待处理的 entry 的数量。

UnansweredBytesCount:当前尚未处理的二进制数据的数量。

 

kratos 在进行同步或恢复时输出的状态信息,代码中通过比较 Metrics.RdbSendSize 和 Metrics.RdbReceivedSize 的大小来判断当前是否收到了 RDB 文件。如果 Metrics.RdbFileSize 等于0,则说明源数据库正在执行 bgsave。如果 Metrics.RdbSendSize 大于 Metrics.RdbReceivedSize,则输出 "receiving rdb",并输出 rdb 文件传输百分比、rdb 文件大小和已接收的 rdb 数据大小的信息。如果 Metrics.RdbFileSize 大于Metrics.RdbSendSize,则输出 "syncing rdb",并输出 rdb 同步百分比、每秒允许同步的操作数、每秒拒绝同步的操作数、当前同步的 Entry id、还在等待处理的 Entry 数量、尚未处理的二进制数据量、rdb 文件大小和已发送的 rdb 数据量的信息。如果 Metrics.RdbSendSize 等于 Metrics.RdbFileSize,则说明 RDB 文件已经同步完成,输出 "syncing aof",并输出每秒允许同步的操作数、每秒拒绝同步的操作数、当前同步的 Entry id、还在等待处理的 Entry 数量、尚未处理的二进制数据量、已应用但还未执行的 AOF 差异(即 AOFReceivedOffset - AOFAppliedOffset)、已接收的 AOF 数据偏移量和已应用的 AOF 数据偏移量信息。其中,字符串格式化函数fmt.Sprintf 用于生成这些 log 信息,日志输出函数 log.Infof 用于输出它们。

 剩下还有一些函数

 这部分是 kratos 中定义了一些更新指标的函数,包括允许同步操作数、拒绝同步操作数、entry id、RDB 文件大小、已接收的 RDB 大小、已发送的 RDB 偏移量、已接收和已应用的 AOF 偏移量、等待处理的 entry 数量以及待回答的二进制数据量。这些函数可以用于更新指标并在日志输出时展示它们。

crc.go系列文件

如CRC64校验算法。CRC64校验算法与CRC16类似,但更适合于大容量的数据校验,因为它使用更宽的位宽(64位)和更强的散列函数来减少冲突。

校验和

utils中的file.go

 

实现了一个用于检查文件是否存在的工具函数DoesFileExist。该函数接受一个字符串类型的文件名作为输入,并返回一个布尔值,表示该文件是否存在于文件系统中。

 

writer中interface.go

短短几行

redis.go

 

定义了redisWriter类型,它实现了Writer接口。redisWriter类型有以下字段:

client:连接Redis实例的客户端;

DbId:指针指向当前Redis实例要使用的数据库编号;

cmdBuffer:用于构造指令的缓冲区;

chWaitReply:管道用于记录回复的Entry实例;

chWg:等待组,用于确认flushInterval协程已释放;

UpdateUnansweredBytesCount:待发送未回答的字节数。

newrediswriter():

创建redisWriter实例。它会连接到Redis实例,并初始化其他redisWriter字段,如cmdBuffer和chWaitReply。此外,该函数还创建一个flushInterval协程,该协程定期检查管道中是否有等待响应的条目,并根据需要刷新Redis实例以确保写入缓冲区数据能提交到Redis实例。

Write方法用于将entry.Entry实例写入Redis实例。该方法首先会检查entry.Entry实例所属的数据库是否已经选择,如果没有选择则需要先通过switchDbTo方法切换到正确的数据库。然后,该方法将entry.Entry实例的内容编码到cmdBuffer中,用于在之后发送到Redis实例。在发送之前,该方法还会检查已发送但没有接收到响应的命令大小(即UpdateUnansweredBytesCount)是否超过了Redis客户端计算出的最大查询缓冲区限制。如果是,则该方法将等待短时间,直到缓冲区空间足够。之后,该方法将entry.Entry实例写入chWaitReply管道中,增加UpdateUnansweredBytesCount计数器,最后通过客户端发送命令。

 switchDbTo:用于切换到传入的新数据库。它会构造一个select指令,并通过客户端发送到Redis实例。

 flushInterval():是一个单独的协程,用于定期检查管道中是否有等待响应的entry.Entry实例,并根据需要刷新Redis实例,以确保写入缓冲区数据能提交到Redis实例。具体来说,该方法会循环从管道中获取entry.Entry实例,并通过客户端接收回复。根据接收到的回复,该方法会进行相应处理,如记录警告或异常等。此外,该方法还会更新UpdateUnansweredBytesCount计数器、统计AOF已应用的偏移量并更新未响应的字节数统计。

Close():用于关闭redisWriter实例。它会先关闭chWaitReply管道,以使flushInterval协程能够正常退出。然后,它将等待flushInterval协程退出的完成信号。

redis_cluster.go

 NewRedisClusterWriter函数用于创建RedisClusterWriter实例。它会尝试连接Redis集群中所有节点,并使用地址列表和连接信息初始化redisWriter实例。如果出现连接错误,则会记录日志并终止程序。在返回之前,该函数会打印出已成功连接的Redis节点地址。

 

 

loadClusterNodes函数是RedisClusterWriter的初始化函数,用于从Redis集群中的一个节点加载集群信息。loadClusterNodes连接到给定地址的节点,并向其发送cluster nodes命令以获取集群信息。该命令会返回所有的Redis节点及其状态,每个状态以一行输出。RedisClusterWriter遍历所有状态行,并解析它们来获取有用的信息。当解析完成后,RedisClusterWriter将创建一个writer列表,以便在路由entry.Entry数据时使用。此外,RedisClusterWriter还会填充router数组,以便能够将数据路由到正确的节点。

具体而言,loadClusterNodes方法按以下步骤执行:

创建一个新的redis client连接到给定地址的Redis节点。

发送cluster nodes命令并获取返回结果,将结果进行清理。

遍历结果中的每个行,并提取其节点信息。

将每个节点的地址和活动写入添加到RedisClusterWriter的地址和writers列表中。

解析每个节点的槽位信息,并动态填充在router数组中。

检查router所有的槽位是否都被占据,如果有未被占据的槽位,该方法将抛出一个panic异常。

Write():用于将entry.Entry数据写入到Redis集群中。如果entry.Entry中的Slots为空,则直接将数据写入到Redis集群中的所有节点;如果entry.Entry中的Slots不为空,则按照entry.Entry中的Slots所指定的槽位,将数据路由到对应的Redis节点中。

具体而言,Write方法按以下步骤执行:

如果entry.Entry中的Slots为空,则将entry.Entry数据写入到Redis集群中的所有节点中,然后返回。

如果entry.Entry中的Slots不为空,则检查所有槽位是否都在同一个槽区中。如果不是,则抛出一个CROSSSLOT错误。

如果entry.Entry中的Slots都在同一个槽区中,则根据槽区选择要写入数据的Redis节点。具体而言,RedisClusterWriter会找到最后一个槽位,然后使用对应的Redis节点的writer将entry.Entry数据写入到Redis节点中。

Close()用于关闭RedisClusterWriter的所有Redis写入器。它会迭代所有的writer,并调用每个writer的Close方法来关闭它们。

 本人新人 部分资料可能有误 请见谅

 

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值