go手写Redis(9)之Aof消息落盘和消息重放

Aof持久化

用户在发送指令之后,涉及到写和修改数据的指令以及切换数据库的指令都需要进行持久化避免消息的丢失,下面我们来实现一下,代码定义在 aof/aof.go

1. payload

对传递进行来的参数和数据索引进行包装一下,便于后续投递到管道中

//CmdLine 二维数组的别名
type CmdLine = [][]byte

type payload struct {
	cmdLine CmdLine
	dbIndex int
}

2. AofHandler

字段说明

  • database:数据库核心,前面数据库实现中已经实现了
  • aofChan:管道用于投放
  • aofFile:文件流
  • aofFilename:aof文件的名称
  • currentDB:当前数据库的索引,如果出现了切换数据库的命令,那么这里也需要进行更改
//AofHandler 处理器
type AofHandler struct {
	database database.Database
	aofChan  chan *payload
	//打开文件流
	aofFile     *os.File
	aofFilename string
	//表示指令中当前使用的db索引
	currentDB int
}
//NewAofHandler 创建一个新的aof结构体
func NewAofHandler(database database.Database) (*AofHandler, error) {
	handler := &AofHandler{}
	handler.aofFilename = config.Properties.AppendFilename
	handler.database = database
	//恢复aof文件中已经写入的数据
	handler.loadAof()
	//打开文件,指定文件的模式,可以新建可以读写
	aofFile, err := os.OpenFile(handler.aofFilename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0600)
	if err != nil {
		return nil, err
	}
	handler.aofFile = aofFile
	//数据引擎在写入的数据的时候需要不停的写入channel中然后追加到aof中,设置一个缓冲区
	handler.aofChan = make(chan *payload, aofBufferSize)
	//开启协程处理数据
	go func() {
		handler.handlerAof()
	}()
	return handler, nil
}

3. 消息落盘

消息落盘的功能主要在 AddAof、handlerAof 两个方法中

  • AddAof:像管道中追加用户发送的命令,在 Database进行初始化时,我们就将这个函数赋值给了 DB中的 addAof 这个变量,具体使用可以看 database/database.go 中的NewDatabase()
  • handlerAof:在 NewAofHandler() 中开启了一个协程直接异步调用监听 aofChan 这个管道中的数据,然后直接通过 aofFile 直接写入文件中
//AddAof 追加数据到aof的channel中
func (handler *AofHandler) AddAof(dbIndex int, cmd CmdLine) {
	//是否开启了aof功能并且aof管道已经初始化了
	appendOnly := config.Properties.AppendOnly && handler.aofChan != nil
	if !appendOnly {
		return
	}
	//往aof管道中追加数据
	handler.aofChan <- &payload{
		dbIndex: dbIndex,
		cmdLine: cmd,
	}
}

//handlerAof 消息落盘,需要从channel中读取数据最后写入磁盘中
func (handler *AofHandler) handlerAof() {
	//遍历管道数据
	handler.currentDB = 0
	for p := range handler.aofChan {
		//判断当前指令所使用的数据库索引跟上一次的数据库索引是否一样(出现切换了数据库逻辑)
		if p.dbIndex != handler.currentDB {
			//如果数据库不一样,需要向文件中追加一个选择数据库的命令 select 1
			data := reply.MakeMultiBulkReply(utils.ToCmdLine("select", strconv.Itoa(p.dbIndex))).ToBytes()
			_, err := handler.aofFile.Write(data)
			if err != nil {
				logger.Error(err)
				//如果出现异常需要继续执行
				continue
			}
			handler.currentDB = p.dbIndex
		}
		//写入aof文件中
		data := reply.MakeMultiBulkReply(p.cmdLine).ToBytes()
		_, err := handler.aofFile.Write(data)
		if err != nil {
			logger.Error(err)
		}
	}
}

4. 消息重放

在启动 aof 功能时,我们需要将 aof文件中的所有数据全部重新加载一次,加载到内存当中,调用是在 NewAofHandler() 方法中

//loadAof 重新执行aof对数据进行还原
func (handler *AofHandler) loadAof() {
	//按照只读的方式打开,简化版的OpenFile()方法
	aofFile, err := os.Open(handler.aofFilename)
	if err != nil {
		logger.Error(err)
		return
	}
	//注册一个关闭
	defer func(aofFile *os.File) {
		_ = aofFile.Close()
	}(aofFile)
	//直接通过协议解析流获取到管道,aofFile也是实现了 io.Reader 这个接口
	ch := parser.ParseStream(aofFile)
	//创建一个假的连接对象可以服用原来的exec()方法
	fackConn := &connection.Connection{}
	//读取管道的数据
	for p := range ch {
		if p.Err != nil {
			//EOF文件的结束符,直接退出
			if p.Err == io.EOF {
				break
			}
			//不是EOF结束符们直接打印错误信息,继续下一行
			logger.Error(p.Err)
			continue
		}
		if p.Data == nil {
			//空数据的错误
			logger.Error("empty payload")
			continue
		}
		//所有记录的数据都是二维数组的对象
		r, ok := p.Data.(*reply.MultiBulkReply)
		if !ok {
			logger.Error("need multi mulk")
			continue
		}
		rep := handler.database.Exec(fackConn, r.Args)
		//判断是否是错误回复
		if reply.IsErrReply(rep) {
			logger.Error(rep)
		}
	}
}

你可能感兴趣的:(GO,redis,golang)