redisshake的主要流程

在之前介绍了redisshake的源码 今天就来详细说一下redisshake的主要流程(main.go)

首先是源码

func main() {
	if len(os.Args) < 2 || len(os.Args) > 3 {
		fmt.Println("Usage: redis-shake <config file> <filter file>")
		fmt.Println("Example: redis-shake config.toml filter.lua")
		os.Exit(1)
	}
//检查命令行参数 看合不合规范

	// load filter file
	if len(os.Args) == 3 {
		luaFile := os.Args[2]
		filter.LoadFromFile(luaFile)
	}
//参数为3 加载过滤器文件

	// load config
	configFile := os.Args[1]
	config.LoadFromFile(configFile)
//加载配置文件
	log.Init()
	log.Infof("GOOS: %s, GOARCH: %s", runtime.GOOS, runtime.GOARCH)
	log.Infof("Ncpu: %d, GOMAXPROCS: %d", config.Config.Advanced.Ncpu, runtime.GOMAXPROCS(0))
	log.Infof("pid: %d", os.Getpid())
	log.Infof("pprof_port: %d", config.Config.Advanced.PprofPort)
//初始化日志模块
	if len(os.Args) == 2 {
		log.Infof("No lua file specified, will not filter any cmd.")
	}
//如果命令行参数个数为2,则表示没有指定Lua脚本文件,代码会输出一条日志信息。
	// start pprof
	if config.Config.Advanced.PprofPort != 0 {
		go func() {
			err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Config.Advanced.PprofPort), nil)
			if err != nil {
				log.PanicError(err)
			}
		}()
	}
//启动pprof性能分析服务器。
	// start statistics
	if config.Config.Advanced.MetricsPort != 0 {
		statistics.Metrics.Address = config.Config.Source.Address
		go func() {
			log.Infof("metrics url: http://localhost:%d", config.Config.Advanced.MetricsPort)
			mux := http.NewServeMux()
			mux.HandleFunc("/", statistics.Handler)
			err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Config.Advanced.MetricsPort), mux)
			if err != nil {
				log.PanicError(err)
			}
		}()
	}
//启动metrics服务器,并提供收集Redis数据同步信息的接口。
	// create writer
	var theWriter writer.Writer
	target := &config.Config.Target
	switch config.Config.Target.Type {
	case "standalone":
		theWriter = writer.NewRedisWriter(target.Address, target.Username, target.Password, target.IsTLS)
	case "cluster":
		theWriter = writer.NewRedisClusterWriter(target.Address, target.Username, target.Password, target.IsTLS)
	default:
		log.Panicf("unknown target type: %s", target.Type)
	}
//创建一个写入器,可以是独立模式的Redis或集群模式的Redis。

	// create reader
	source := &config.Config.Source
	var theReader reader.Reader
	if config.Config.Type == "sync" {
		theReader = reader.NewPSyncReader(source.Address, source.Username, source.Password, source.IsTLS, source.ElastiCachePSync)
	} else if config.Config.Type == "restore" {
		theReader = reader.NewRDBReader(source.RDBFilePath)
	} else if config.Config.Type == "scan" {
		theReader = reader.NewScanReader(source.Address, source.Username, source.Password, source.IsTLS)
	} else {
		log.Panicf("unknown source type: %s", config.Config.Type)
	}
	ch := theReader.StartRead()
//创建一个读取器,可以是PSync、Restore或Scan
//创建一个通道(ch)用于接收读取到的数据
	// start sync
	statistics.Init()
//初始化统计信息    
	id := uint64(0)
//进入一个循环,不断从通道(ch)中接收读取到的数据    
	for e := range ch {
		statistics.UpdateInQueueEntriesCount(uint64(len(ch)))
		// calc arguments
		e.Id = id
		id++
		e.CmdName, e.Group, e.Keys = commands.CalcKeys(e.Argv)
		e.Slots = commands.CalcSlots(e.Keys)

		// filter
		code := filter.Filter(e)
		statistics.UpdateEntryId(e.Id)
		if code == filter.Allow {
			theWriter.Write(e)
			statistics.AddAllowEntriesCount()
		} else if code == filter.Disallow {
			// do something
			statistics.AddDisallowEntriesCount()
		} else {
			log.Panicf("error when run lua filter. entry: %s", e.ToString())
		}
	}
	theWriter.Close()
	log.Infof("finished.")
}

整体步骤:

  1. 检查命令行参数,符不符合规范

  2. 加载过滤器文件

  3. 加载配置文件

  4. 初始化日志模块

  5. 如果命令行参数个数为2,则表示没有指定Lua脚本文件

  6. 如果有pprof端口,则启动pprof服务器

  7. 如果有metrics端口,则启动metrics服务器,并提供收集Redis数据同步信息的接口

  8. 创建一个写入器,可以是独立模式或集群模式

  9. 创建一个读取器,PSync、Restore或Scan

  10. theReader的StartRead,返回一个通道(ch)用于接收读取到的数据

  11. 初始化统计信息(statistics)

  12. 进入一个循环,不断从通道(ch)中接收读取到的数据,并处理数据

 

目录

整体步骤:

一.检查命令行参数

二.加载过滤器文件

三.加载配置文件

1.解析TOML文件

2.创建并使用目录

3.设置CPU核心数量

4.检查Redis版本

5.检查配置类型

四.初始化日志模块

五.如果有pprof端口,则启动pprof服务器

六.如果有metrics端口,则启动metrics服务器

七.创建写入器

1.redisclient

2.rediswriter

八.创建读取器

sync

九.初始化统计信息(statistics)

十.不断从ch中接收读取到的数据,并处理


一.检查命令行参数

if len(os.Args) < 2 || len(os.Args) > 3 {
	fmt.Println("Usage: redis-shake <config file> <filter file>")
	fmt.Println("Example: redis-shake config.toml filter.lua")
	os.Exit(1)
}

这里限定Args的个数为3

这里os.Args[1]为配置文件

os.Arg[2]为过滤文件

程序需要两个参数:一个配置文件和一个可选的过滤文件。如果参数数量不符合这个要求,程序将直接退出。

二.加载过滤器文件

if len(os.Args) == 3 {
		luaFile := os.Args[2]
		filter.LoadFromFile(luaFile)
	}
func LoadFromFile(luaFile string) {
	luaInstance = lua.NewState()
	err := luaInstance.DoFile(luaFile)
	if err != nil {
		panic(err)
	}
}

这里的加载文件没什么好说的    加载 Lua 代码,存到一个 Lua 实例

三.加载配置文件

	configFile := os.Args[1]
	config.LoadFromFile(configFile)

func LoadFromFile(filename string) {

	buf, err := ioutil.ReadFile(filename)
	if err != nil {
		panic(err.Error())
	}

	decoder := toml.NewDecoder(bytes.NewReader(buf))
	decoder.SetStrict(true)
	err = decoder.Decode(&Config)
	if err != nil {
		missingError, ok := err.(*toml.StrictMissingError)
		if ok {
			panic(fmt.Sprintf("decode config error:\n%s", missingError.String()))
		}
		panic(err.Error())
	}

	// dir
	err = os.MkdirAll(Config.Advanced.Dir, os.ModePerm)
	if err != nil {
		panic(err.Error())
	}
	err = os.Chdir(Config.Advanced.Dir)
	if err != nil {
		panic(err.Error())
	}
//通过 os.MkdirAll() 进行目录的创建,以及通过 os.Chdir() 设置工作目录。
	// cpu core
	var ncpu int
	if Config.Advanced.Ncpu == 0 {
		ncpu = runtime.NumCPU()
	} else {
		ncpu = Config.Advanced.Ncpu
	}
	runtime.GOMAXPROCS(ncpu)
//通过 runtime.GOMAXPROCS() 函数设置并发执行环境中的 CPU 核心数量。
	if Config.Source.Version < 2.8 {
		panic("source redis version must be greater than 2.8")
	}
	if Config.Target.Version < 2.8 {
		panic("target redis version must be greater than 2.8")
	}

	if Config.Type != "sync" && Config.Type != "restore" && Config.Type != "scan" {
		panic("type must be sync/restore/scan")
	}
}

1.解析TOML文件

decoder := toml.NewDecoder(bytes.NewReader(buf))
	decoder.SetStrict(true)
	err = decoder.Decode(&Config)
	if err != nil {
		missingError, ok := err.(*toml.StrictMissingError)
		if ok {
			panic(fmt.Sprintf("decode config error:\n%s", missingError.String()))
		}
		panic(err.Error())
	}

读取buf后 将文件内容buf解码到Config结构体中

由于本人才疏学浅 没用过TOML文件 这里百度一下

“TOML,全称Tom’s Obvious, Minimal Language,是一个易于阅读和编写的最小化配置文件格式,由Tom Preston-Werner创建。它设计为清晰无歧义,并且映射到哈希表。这使得它对于配置文件、配置数据、交换数据等用途都非常有用。”

2.创建并使用目录

err = os.MkdirAll(Config.Advanced.Dir, os.ModePerm)
	if err != nil {
		panic(err.Error())
	}
	err = os.Chdir(Config.Advanced.Dir)
	if err != nil {
		panic(err.Error())
	}

使用os.MkdirAll创建目录,

然后使用os.Chdir将程序的工作目录改为刚刚创建的目录。

3.设置CPU核心数量

	// cpu core
	var ncpu int
	if Config.Advanced.Ncpu == 0 {
		ncpu = runtime.NumCPU()
	} else {
		ncpu = Config.Advanced.Ncpu
	}
	runtime.GOMAXPROCS(ncpu)

设置Go运行环境可以使用的最大CPU核心数量。

既可以充分利用机器的CPU资源,也允许用户通过配置来限制CPU利用率,避免程序过度占用系统资源...

4.检查Redis版本

if Config.Source.Version < 2.8 {
panic(“source redis version must be greater than 2.8”)
}
if Config.Target.Version < 2.8 {
panic(“target redis version must be greater than 2.8”)
}

无论代码检查源Redis还是目标Redis的版本,不允许任一版本低于2.8

5.检查配置类型

if Config.Type != "sync" && Config.Type != "restore" && Config.Type != "scan" {
		panic("type must be sync/restore/scan")
	}

Config.Type必须是"sync",“restore”,或"scan"中的一个 毕竟这是redisshake的特色之一

四.初始化日志模块

	log.Init()
	log.Infof("GOOS: %s, GOARCH: %s", runtime.GOOS, runtime.GOARCH)
	log.Infof("Ncpu: %d, GOMAXPROCS: %d", config.Config.Advanced.Ncpu, runtime.GOMAXPROCS(0))
	log.Infof("pid: %d", os.Getpid())
	log.Infof("pprof_port: %d", config.Config.Advanced.PprofPort)

 CPU 核心数  输出程序进程的 ID(PID)  Pprof 的端口号...

五.如果有pprof端口,则启动pprof服务器

// start pprof
	if config.Config.Advanced.PprofPort != 0 {
		go func() {
			err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Config.Advanced.PprofPort), nil)
			if err != nil {
				log.PanicError(err)
			}
		}()
	}

 http.ListenAndServe 创建一个服务器,监听。函数的第一个参数是监听的地址

第二个参数为处理 HTTP 请求的处理器

但是本人也没用过pprof端口(可怜) 百度了一下

“启动服务器后,pprof 会开始收集并提供关于程序性能和调试的信息,例如堆栈跟踪、内存分配、goroutine 等。这样,开发人员可以通过访问指定的端口来调用和分析这些信息,从而进行性能分析和优化。”

六.如果有metrics端口,则启动metrics服务器

// start statistics
	if config.Config.Advanced.MetricsPort != 0 {
		statistics.Metrics.Address = config.Config.Source.Address
		go func() {
			log.Infof("metrics url: http://localhost:%d", config.Config.Advanced.MetricsPort)
			mux := http.NewServeMux()
			mux.HandleFunc("/", statistics.Handler)
			err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Config.Advanced.MetricsPort), mux)
			if err != nil {
				log.PanicError(err)
			}
		}()
	}

首先如果 MetricsPort 不为零,表示需要启用该服务。

接下来将 Address 赋值给 statistics.Metrics.Address。这个地址通常被用于获取指标数据,作为指标服务的目标地址。

然后是使用匿名函数和 goroutine(不会阻塞主程序的执行):

1.log.Infof 打印一条日志信息,表示指标服务的 URL。

2.创建一个新的路由器http.NewServeMux()作为 HTTP 请求的处理器。

3.使用 mux.HandleFunc绑定路径 所有该路径的请求将由 statistics.Handler 函数处理。

4.http.ListenAndServe 函数来创建一个服务器,监听指定端口 第一个参数是监听的地址 第二个参数是之前创建的路由器

百度了一下metrics服务

“监听 HTTP 请求,通过访问相应的 URL,可以获取与程序性能相关的指标数据。”

然后这里还出现了statistics这个包 也是redisshake的.go文件

 结合源码可知 该包其实就是统计Redis 同步服务的各种指标

func Handler(w http.ResponseWriter, _ *http.Request) {
	w.Header().Add("Content-Type", "application/json")
	err := json.NewEncoder(w).Encode(Metrics)
	if err != nil {
		log.PanicError(err)
	}
}

用于将 metrics 结构体以 JSON 格式输出到 HTTP 响应中。

七.创建写入器

	// create writer
	var theWriter writer.Writer
	target := &config.Config.Target
	switch config.Config.Target.Type {
	case "standalone":
		theWriter = writer.NewRedisWriter(target.Address, target.Username, target.Password, target.IsTLS)
	case "cluster":
		theWriter = writer.NewRedisClusterWriter(target.Address, target.Username, target.Password, target.IsTLS)
	default:
		log.Panicf("unknown target type: %s", target.Type)
	}

分单机和集群模式 当然肯定要看它这么创建的 这里用NewRedisWriter举例

type redisWriter struct {
	client *client.Redis
	DbId   int

	cmdBuffer   *bytes.Buffer
	chWaitReply chan *entry.Entry
	chWg        sync.WaitGroup

	UpdateUnansweredBytesCount uint64 // have sent in bytes
}

func NewRedisWriter(address string, username string, password string, isTls bool) Writer {
	rw := new(redisWriter)
	rw.client = client.NewRedisClient(address, username, password, isTls)
	log.Infof("redisWriter connected to redis successful. address=[%s]", address)
	rw.cmdBuffer = new(bytes.Buffer)
	rw.chWaitReply = make(chan *entry.Entry, config.Config.Advanced.PipelineCountLimit)
	rw.chWg.Add(1)
	go rw.flushInterval()
	return rw
}

      我们看到这里redisWriter结构体里面有 client 以及下面函数都使用了client 所以先讲client

1.redisclient

我们要创建一个客户端 方便我们与redis服务器通信

type Redis struct {
	reader      *bufio.Reader
	writer      *bufio.Writer
	protoReader *proto.Reader
	protoWriter *proto.Writer
}

4个读写器 分别用于从 Redis 连接中读写数据 用于解析从 Redis 连接读写的数据,并进行协议处理。

func NewRedisClient(address string, username string, password string, isTls bool) *Redis {    //创建新的Redis客户端
	r := new(Redis)
//网络连接接口和网络拨号器,
	var conn net.Conn
	var dialer net.Dialer
	var err error
//设置连接超时时间
	dialer.Timeout = 3 * time.Second
	if isTls {
//创建网络链接并判断是否使用TLS连接
		conn, err = tls.DialWithDialer(&dialer, "tcp", address, &tls.Config{InsecureSkipVerify: true})
	} else {
		conn, err = dialer.Dial("tcp", address)
	}
	if err != nil {
		log.PanicError(err)
	}
//初始化Redis读写器(与conn绑定
	r.reader = bufio.NewReader(conn)
	r.writer = bufio.NewWriter(conn)
	r.protoReader = proto.NewReader(r.reader)
	r.protoWriter = proto.NewWriter(r.writer)

	// auth
//进行Redis授权
	if password != "" {
		var reply string
		if username != "" {
			reply = r.DoWithStringReply("auth", username, password)
		} else {
			reply = r.DoWithStringReply("auth", password)
		}
		if reply != "OK" {
			log.Panicf("auth failed with reply: %s", reply)
		}
		log.Infof("auth successful. address=[%s]", address)
	} else {
		log.Infof("no password. address=[%s]", address)
	}

	// ping to test connection
//测试连接
	reply := r.DoWithStringReply("ping")

	if reply != "PONG" {
		panic("ping failed with reply: " + reply)
	}

	return r
}

new(Redis)创建一个新的Redis实例

定义了conndialerconn是一个网络连接接口,dialer是一个网络拨号器,用于建立网络连接。

设置连接超时时间 超时就停止尝试链接

判断是否使用TLS连接 绑定地址

使用bufio.NewReaderbufio.NewWriter创建了两个用于读写网络数据的缓冲区,并根据这个缓冲区创建协议读写器,用于读写Redis协议的数据 绑定conn

进行Redis授权

测试连接,返回结果

这段代码就是创建了一个绑定了conn和地址的redis客户端 而且初始化的读写器 那我们继续看看rediswritter

2.rediswriter

type redisWriter struct {
	client *client.Redis
	DbId   int

	cmdBuffer   *bytes.Buffer
	chWaitReply chan *entry.Entry
	chWg        sync.WaitGroup

	UpdateUnansweredBytesCount uint64 // have sent in bytes
}

client  刚刚介绍了

DbId 一个整数字段,记录写入数据时使用的 Redis 数据库的 ID

cmdBuffer 用于缓冲待发送的 Redis 命令

chWaitReply 用于等待 Redis 命令的回复

chWg 用于等待 goroutine 完成特定任务的同步操作

UpdateUnansweredBytesCount 记录已发送但尚未得到回复的字节数

sync.WaitGroup

提供以下三个函数

Add(delta int):用于将计数器增加 计数,表示要等待的任务数量增加了。

Done():用于将计数器减少 1,表示一个任务已完成。

Wait():阻塞调用的goroutine,直到计数器减少为 0。这意味着所有的任务都已完成。

总的来说 这个结构体的类型配合使用,可以实现与 Redis 服务器进行写入操作并等待回复的功能。

func NewRedisWriter(address string, username string, password string, isTls bool) Writer {
	rw := new(redisWriter)
	rw.client = client.NewRedisClient(address, username, password, isTls)
	log.Infof("redisWriter connected to redis successful. address=[%s]", address)
	rw.cmdBuffer = new(bytes.Buffer)
	rw.chWaitReply = make(chan *entry.Entry, config.Config.Advanced.PipelineCountLimit)
	rw.chWg.Add(1)
	go rw.flushInterval()
	return rw
}

 首先,创建了一个新的 redisWriter 结构体实例,并将其赋值给变量 rw

 通过 传入地址、用户名、密码和 TLS 信息,创建一个 Redis 客户端实例。

使用 new 创建一个新的 bytes.Buffer 实例,并将其赋值给 rw.cmdBuffer 字段。用于缓冲待发送的 Redis 命令。

 make 创建一个带有容量限制的通道 rw.chWaitReply,该通道用于接收 Redis 命令的回复。

调用rw.chWg.Add(1) 将任务计数器增加 1,以表示有一个任务需要在后台 goroutine 中完成

 go rw.flushInterval() 用于定期刷新数据到 Redis 服务器。

最后返回 rw,作为 Writer 接口

这就是创建写入器的流程

八.创建读取器

source := &config.Config.Source
var theReader reader.Reader

if config.Config.Type == "sync" {
	theReader = reader.NewPSyncReader(source.Address, source.Username, source.Password, source.IsTLS, source.ElastiCachePSync)
} else if config.Config.Type == "restore" {
	theReader = reader.NewRDBReader(source.RDBFilePath)
} else if config.Config.Type == "scan" {
	theReader = reader.NewScanReader(source.Address, source.Username, source.Password, source.IsTLS)
} else {
	log.Panicf("unknown source type: %s", config.Config.Type)
}

ch := theReader.StartRead()

根据配置文件中的类型选择相应的读取器,并使用读取器开始读取数据,并将读取的数据发送到通道 ch(对应redisshake的三大类型)

  以NewPSyncReader为例

sync

首先看代码


type psyncReader struct {
	client  *client.Redis
	address string
	ch      chan *entry.Entry
	DbId    int

	rd               *bufio.Reader
	receivedOffset   int64
	elastiCachePSync string
}

client 表示与 Redis 服务器进行通信的客户端 前面提到过

address 表示要连接的 Redis 服务器的地址

ch 用于接收读取到的 Redis 数据

DbId 读取数据时要使用的 Redis 数据库的 ID

rd  用于从 Redis 服务器进行数据读取。而且它提供了缓冲读取功能

receivedOffset 用于记录已接收的数据的偏移量。它表示从 Redis 服务器接收到的数据的位置

elastiCachePSync 雀氏不知道什么东西 查了一下 用于标识是否使用 Elasticache 的 PSYNC 协议?“elastiCache 是指亚马逊 AWS 云平台上提供的一项托管式 Redis 服务。”

总的来说 创建了一个具有与 Redis 服务器进行同步读取数据的能力的读取器。

func NewPSyncReader(address string, username string, password string, isTls bool, ElastiCachePSync string) Reader {
	r := new(psyncReader)
	r.address = address
	r.elastiCachePSync = ElastiCachePSync
	r.client = client.NewRedisClient(address, username, password, isTls)
	r.rd = r.client.BufioReader()
	log.Infof("psyncReader connected to redis successful. address=[%s]", address)
	return r
}

 绑定address ElastiCachePSync(上面提到过)

调用 BufioReader,获取一个 bufio.Reader 对象 用来读取

总的来说 创建一个具有读取Redis服务器数据能力的psyncReader对象

然后就是启动 读取器

func (r *psyncReader) StartRead() chan *entry.Entry {
	r.ch = make(chan *entry.Entry, 1024)

	go func() {
		r.clearDir()
		go r.sendReplconfAck()
		r.saveRDB()
		startOffset := r.receivedOffset
		go r.saveAOF(r.rd)
		r.sendRDB()
		time.Sleep(1 * time.Second) // wait for saveAOF create aof file
		r.sendAOF(startOffset)
	}()

	return r.ch
}

 创建一个容量为 1024r.ch缓冲通道,用于接收读取到的 Redis 数据。

启动一个 goroutine

r.clearDir(): 清理相关目录,准备接收数据。

sendReplconfAck():用于向主节点发送确认信息(ack)。

saveRDB():用于读取 Redis RDB 文件并发送给 r.ch 通道。

saveAOF:接收 Redis 服务器发送的 AOF数据,并将其保存到本地磁盘上。毕竟现版本并不支持AOF恢复,只是直接保存整个文件

sendRDB:用于发送 RDB 数据到从节点。

sendAOF:从Redis服务器读取数据 然后将这个对象送到通道 发送出去(当前版本不支持AOF读取,所以肯定不是redisshake解析的AOF)

这就是启动一个后台任务来读取 Redis 数据的过程

接下来是刚刚各个函数 不想看可以直接跳过

func (r *psyncReader) clearDir() {
	files, err := ioutil.ReadDir("./")
//ioutil.ReadDir("./") 函数读取当前目录下的所有文件和子目录
	if err != nil {
		log.PanicError(err)
	}

	for _, f := range files {//确定当前文件是否以 “.rdb” 或 “.aof” 结尾
		if strings.HasSuffix(f.Name(), ".rdb") || strings.HasSuffix(f.Name(), ".aof") {
			err = os.Remove(f.Name())//如果是“.rdb” 或 “.aof” 就remove os.Remove(f.Name())
			if err != nil {
				log.PanicError(err)
			}
			log.Warnf("remove file. filename=[%s]", f.Name())
		}
	}
}
//清理旧数据,确保收到的数据是最新的
func (r *psyncReader) sendReplconfAck() {
	for range time.Tick(time.Millisecond * 100) {
//每 100 毫秒执行一次下面的代码块
		// send ack receivedOffset
		r.client.Send("replconf", "ack", strconv.FormatInt(r.receivedOffset, 10))
//通过发送 ACK 并传递数据偏移量,表示成功接收并处理了到达的数据
	}
}

func (r *psyncReader) saveRDB() {
	log.Infof("start save RDB. address=[%s]", r.address)
//创建一个字符串,包含要传递给Redis客户端的参数,监听端口为10007
	argv := []string{"replconf", "listening-port", "10007"} // 10007 is magic number
	log.Infof("send %v", argv)
	reply := r.client.DoWithStringReply(argv...)
//使用Redis客户端发送命令,并得到命令执行的回复结果。
	if reply != "OK" {
		log.Warnf("send replconf command to redis server failed. address=[%s], reply=[%s], error=[]", r.address, reply)
	}

	// send psync
	argv = []string{"PSYNC", "?", "-1"}
//PSYNC命令和参数
	if r.elastiCachePSync != "" {
		argv = []string{r.elastiCachePSync, "?", "-1"}
	}
	r.client.Send(argv...)
//使用Redis客户端发送PSYNC命令
	log.Infof("send %v", argv)
	// format: \n\n\n$<reply>\r\n
//一个无限循环,用于接收PSYNC命令的回复
	for true {
		// \n\n\n$
		b, err := r.rd.ReadByte()
		if err != nil {
			log.PanicError(err)
		}
		if b == '\n' {
			continue
		}
		if b == '-' {//减号 失败
			reply, err := r.rd.ReadString('\n')
			if err != nil {
				log.PanicError(err)
			}
			reply = strings.TrimSpace(reply)
			log.Panicf("psync error. address=[%s], reply=[%s]", r.address, reply)
		}
		if b != '+' {//不是加号 回复无效
			log.Panicf("invalid psync reply. address=[%s], b=[%s]", r.address, string(b))
		}
		break
	}
	reply, err := r.rd.ReadString('\n')
//从读取器 r.rd 中读取一行回复结果
	if err != nil {
		log.PanicError(err)
	}
	reply = strings.TrimSpace(reply)
//移除回复结果中的首尾空白字符
	log.Infof("receive [%s]", reply)
	masterOffset, err := strconv.Atoi(strings.Split(reply, " ")[2])
//从回复结果中解析出主服务器的偏移量
	if err != nil {
		log.PanicError(err)
	}
	r.receivedOffset = int64(masterOffset)
//设置偏移量
	log.Infof("source db is doing bgsave. address=[%s]", r.address)
	statistics.Metrics.IsDoingBgsave = true

	timeStart := time.Now()
//记录开始执行后台保存操作的时间
	// format: \n\n\n$<length>\r\n<rdb>
	for true {
//用于读取RDB数据
		// \n\n\n$
		b, err := r.rd.ReadByte()
		if err != nil {
			log.PanicError(err)
		}
		if b == '\n' {
			continue
		}
		if b != '$' {
//RDB数据格式无效
			log.Panicf("invalid rdb format. address=[%s], b=[%s]", r.address, string(b))
		}
		break
	}
	statistics.Metrics.IsDoingBgsave = false //后台操作完成
	log.Infof("source db bgsave finished. timeUsed=[%.2f]s, address=[%s]", time.Since(timeStart).Seconds(), r.address)
	lengthStr, err := r.rd.ReadString('\n')
//从读取器 r.rd 中读取RDB数据的长度
	if err != nil {
		log.PanicError(err)
	}
	lengthStr = strings.TrimSpace(lengthStr)
	length, err := strconv.ParseInt(lengthStr, 10, 64)
//将长度字符串解析为64位有符号整数
	if err != nil {
		log.PanicError(err)
	}
	log.Infof("received rdb length. length=[%d]", length)
	statistics.SetRDBFileSize(uint64(length))
//印收到的RDB数据长度

	// create rdb file
	rdbFilePath := "dump.rdb"
//定义RDB文件的路径和名称为 dump.rdb
	log.Infof("create dump.rdb file. filename_path=[%s]", rdbFilePath)
	rdbFileHandle, err := os.OpenFile(rdbFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
	if err != nil {
		log.PanicError(err)
	}

	// read rdb
	remainder := length
	const bufSize int64 = 32 * 1024 * 1024 // 32MB
	buf := make([]byte, bufSize)
	for remainder != 0 {
		readOnce := bufSize
//确定每次读取的字节数
		if remainder < readOnce {
			readOnce = remainder
//如果剩余的数据长度小于每次读取的字节数,则修改 readOnce 为剩余的数据长度
		}
		n, err := r.rd.Read(buf[:readOnce])
//从读取器中读取 readOnce 字节的数据,并返回实际读取的字节数
		if err != nil {
			log.PanicError(err)
		}
		remainder -= int64(n)
//更新剩余数据的长度
		statistics.UpdateRDBReceivedSize(uint64(length - remainder))
//更新收到的RDB数据的大小
		_, err = rdbFileHandle.Write(buf[:n])
将读取的数据写入RDB文件
		if err != nil {
			log.PanicError(err)
		}
	}
	err = rdbFileHandle.Close()
	if err != nil {
		log.PanicError(err)
	}
	log.Infof("save RDB finished. address=[%s], total_bytes=[%d]", r.address, length)
}
func (r *psyncReader) saveAOF(rd io.Reader) {
	log.Infof("start save AOF. address=[%s]", r.address)
	// create aof file
	aofWriter := rotate.NewAOFWriter(r.receivedOffset)
//r.receivedOffset 作为起始偏移量
	defer aofWriter.Close()
//defer确保会关闭
	buf := make([]byte, 16*1024) // 16KB is enough for writing file
	for {
//读取数据并将其写入AOF文件
		n, err := rd.Read(buf)
// 读取数据,将读取的字节数存储在 n 
		if err != nil {
			log.PanicError(err)
		}
		r.receivedOffset += int64(n)
//更新 r.receivedOffset,增加读取的字节数
		statistics.UpdateAOFReceivedOffset(uint64(r.receivedOffset))
		aofWriter.Write(buf[:n])
//将读取的数据写入AOF文件
	}
}
单纯的保存文件
func (r *psyncReader) sendRDB() {
	// start parse rdb
	log.Infof("start send RDB. address=[%s]", r.address)
	rdbLoader := rdb.NewLoader("dump.rdb", r.ch)
	r.DbId = rdbLoader.ParseRDB()
	log.Infof("send RDB finished. address=[%s], repl-stream-db=[%d]", r.address, r.DbId)
}
praseRDB是redisshake读取rdb的核心代码这里就不介绍了 
func (r *psyncReader) sendAOF(offset int64) {
	aofReader := rotate.NewAOFReader(offset)
	defer aofReader.Close()
	r.client.SetBufioReader(bufio.NewReader(aofReader))
	for {
		argv := client.A	rrayString(r.client.Receive())
		// select
		if strings.EqualFold(argv[0], "select") {
			DbId, err := strconv.Atoi(argv[1])
			if err != nil {
				log.PanicError(err)
			}
			r.DbId = DbId
			continue
		}

		e := entry.NewEntry()
		e.Argv = argv
		e.DbId = r.DbId
		e.Offset = aofReader.Offset()
		r.ch <- e
	}
}
很直观

九.初始化统计信息(statistics)

	statistics.Init()

statistics 提供关于执行任务进度和状态的信息 redis源码也有类似的实现

unc Init() {
	go func() {
		seconds := config.Config.Advanced.LogInterval
//从配置文件中获取日志记录间隔时间
		if seconds <= 0 {
//检查统计功能被禁用
			log.Infof("statistics disabled. seconds=[%d]", seconds)
		}

		lastAllowEntriesCount := Metrics.AllowEntriesCount
		lastDisallowEntriesCount := Metrics.DisallowEntriesCount
// 分别用于保存上一次允许操作和禁止操作计数的值
		for range time.Tick(time.Duration(seconds) * time.Second) {
//无限循环,每隔一定时间触发一次
//根据当前类型执行
			// scan
			if config.Config.Type == "scan" {
				Metrics.Msg = fmt.Sprintf("syncing. dbId=[%d], percent=[%.2f]%%, allowOps=[%.2f], disallowOps=[%.2f], entryId=[%d], InQueueEntriesCount=[%d], unansweredBytesCount=[%d]bytes",
					Metrics.ScanDbId,
					float64(bits.Reverse64(Metrics.ScanCursor))/float64(^uint(0))*100,
					float32(Metrics.AllowEntriesCount-lastAllowEntriesCount)/float32(seconds),
					float32(Metrics.DisallowEntriesCount-lastDisallowEntriesCount)/float32(seconds),
					Metrics.EntryId,
					Metrics.InQueueEntriesCount,
					Metrics.UnansweredBytesCount)
				log.Infof(strings.Replace(Metrics.Msg, "%", "%%", -1))
				lastAllowEntriesCount = Metrics.AllowEntriesCount
				lastDisallowEntriesCount = Metrics.DisallowEntriesCount
				continue
			}
			// sync or restore
			if Metrics.RdbFileSize == 0 {
				Metrics.Msg = "source db is doing bgsave"
			} else if Metrics.RdbSendSize > Metrics.RdbReceivedSize {
				Metrics.Msg = fmt.Sprintf("receiving rdb. percent=[%.2f]%%, rdbFileSize=[%.3f]G, rdbReceivedSize=[%.3f]G",
					float64(Metrics.RdbReceivedSize)/float64(Metrics.RdbFileSize)*100,
					float64(Metrics.RdbFileSize)/1024/1024/1024,
					float64(Metrics.RdbReceivedSize)/1024/1024/1024)
			} else if Metrics.RdbFileSize > Metrics.RdbSendSize {
				Metrics.Msg = fmt.Sprintf("syncing rdb. percent=[%.2f]%%, allowOps=[%.2f], disallowOps=[%.2f], entryId=[%d], InQueueEntriesCount=[%d], unansweredBytesCount=[%d]bytes, rdbFileSize=[%.3f]G, rdbSendSize=[%.3f]G",
					float64(Metrics.RdbSendSize)*100/float64(Metrics.RdbFileSize),
					float32(Metrics.AllowEntriesCount-lastAllowEntriesCount)/float32(seconds),
					float32(Metrics.DisallowEntriesCount-lastDisallowEntriesCount)/float32(seconds),
					Metrics.EntryId,
					Metrics.InQueueEntriesCount,
					Metrics.UnansweredBytesCount,
					float64(Metrics.RdbFileSize)/1024/1024/1024,
					float64(Metrics.RdbSendSize)/1024/1024/1024)
			} else {
				Metrics.Msg = fmt.Sprintf("syncing aof. allowOps=[%.2f], disallowOps=[%.2f], entryId=[%d], InQueueEntriesCount=[%d], unansweredBytesCount=[%d]bytes, diff=[%d], aofReceivedOffset=[%d], aofAppliedOffset=[%d]",
					float32(Metrics.AllowEntriesCount-lastAllowEntriesCount)/float32(seconds),
					float32(Metrics.DisallowEntriesCount-lastDisallowEntriesCount)/float32(seconds),
					Metrics.EntryId,
					Metrics.InQueueEntriesCount,
					Metrics.UnansweredBytesCount,
					Metrics.AofReceivedOffset-Metrics.AofAppliedOffset,
					Metrics.AofReceivedOffset,
					Metrics.AofAppliedOffset)
			}
			log.Infof(strings.Replace(Metrics.Msg, "%", "%%", -1))
// 打印统计信息的日志消息,其中将百分号 “%” 替换为 “%%” 以避免格式化参数的错误(这个应该都知道吧
			lastAllowEntriesCount = Metrics.AllowEntriesCount
			lastDisallowEntriesCount = Metrics.DisallowEntriesCount
		}
	}()
}

 

十.不断从ch中接收读取到的数据,并处理

id := uint64(0)
	for e := range ch {
		statistics.UpdateInQueueEntriesCount(uint64(len(ch)))
		// calc arguments
		e.Id = id
		id++
		e.CmdName, e.Group, e.Keys = commands.CalcKeys(e.Argv)
		e.Slots = commands.CalcSlots(e.Keys)

		// filter
		code := filter.Filter(e)
		statistics.UpdateEntryId(e.Id)
		if code == filter.Allow {
			theWriter.Write(e)
			statistics.AddAllowEntriesCount()
		} else if code == filter.Disallow {
			// do something
			statistics.AddDisallowEntriesCount()
		} else {
			log.Panicf("error when run lua filter. entry: %s", e.ToString())
		}
	}
	theWriter.Close()
	log.Infof("finished.")
}

首先一直从管道ch中读取事件e

statistics.UpdateInQueueEntriesCount(uint64(len(ch))):更新待处理的事件数统计量

e.Id = id 每一事件的标识为id

id++

计算e的Slots

code := filter.Filter(e) 进行过滤

如果事件被允许,把事件写入,统计

如果事件被不允许,那么不会写入,统计

那么将会报告一个错误,因为过滤结果无法识别

更新统计信息中记录的最新处理的事件id

由此可见 redisshake处理数据 主要靠从ch通道里读取

再来看看过滤是怎么做的

func Filter(e *entry.Entry) int {
	if luaInstance == nil {
		return Allow
	}
	keys := luaInstance.NewTable()
//将e.Keys和e.Slots中的每一个元素分别添加至keys和slots Lua表。
	for _, key := range e.Keys {
		keys.Append(lua.LString(key))
	}

	slots := luaInstance.NewTable()
	for _, slot := range e.Slots {
		slots.Append(lua.LNumber(slot))
	}


	f := luaInstance.GetGlobal("filter")

	luaInstance.Push(f)
//获取全局函数filter 并压栈
	luaInstance.Push(lua.LNumber(e.Id))          // id
	luaInstance.Push(lua.LBool(e.IsBase))        // is_base
	luaInstance.Push(lua.LString(e.Group))       // group
	luaInstance.Push(lua.LString(e.CmdName))     // cmd name
	luaInstance.Push(keys)                       // keys
	luaInstance.Push(slots)                      // slots
	luaInstance.Push(lua.LNumber(e.DbId))        // dbid
	luaInstance.Push(lua.LNumber(e.TimestampMs)) // timestamp_ms
//将entry中的各个属性压入到Lua实例的栈中
	luaInstance.Call(8, 2)

	code := int(luaInstance.Get(1).(lua.LNumber))
	e.DbId = int(luaInstance.Get(2).(lua.LNumber))
//这两行代码取出filter函数的返回值
	luaInstance.Pop(2)
//清空Lua实例的栈。
	return code
}

这个代码首先判断Lua实例是否存在 这在这篇文章目录二里面已经加载了

之所以要用栈 压入到Lua实例的栈中,处理结束之后,再从栈中取出改动后的数据,这样操作很简洁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值