任何应用,只要贴上“系统”二字的标签,那么就一定离不开一个模块,那就是“日志”。既然我们要开发一个数据库系统,那么它就必须要有自己的日志模块。日志通常用来记录系统的运行状态,类似于快照。一旦系统出现异常,管理员或其代码本身可以通过扫描分析日志来确定问题所在,或者通过日志进行错误恢复,这对于数据库系统来说是非常重要的。更重要。数据库系统经常需要从文件中读取和写入大量数据。在此过程中容易出现各种问题。比如交易执行时突然断网,机器突然断电,交易执行到一半就会失败。突然中断,当系统重启时,整个数据库会处于错误状态,即写入了一部分数据,但丢失了一部分数据。这种情况对数据库系统来说是非常致命的。如果不能保证数据的一致性,那么这种数据系统就没人敢用了。如何保证数据的一致性?这取决于日志。数据库在读写数据之前,会先写日志记录相应的操作,比如当前操作是读还是写,然后记录要读写的数据。假设我们现在有一个业务,需要向两个表中写入一百行数据,前50行写入表1,后50行写入表2,那么日志会记录“表1”writes0to50rows”;“Write51to100rowsintable2”这样的信息。假设前50行数据写入后,突然断电,机器重启,数据库系统重启。它自动扫描日志,发现“向表2中写入51到100行”的操作还没有被执行,于是再次执行这个操作,这样可以保证数据的一致性。在本节中,我们将看到如何在上一节中文件系统的实现的基础上实现日志模块。对于日志模块,日志是一组字节数组。它只负责将数组的内容写入内存或磁盘文件。它不关心数据的内容是什么或者格式是如何解析的。同时,日志的写入采用“推送”方式。假设我们有3条日志,长度分别为50字节、100字节和100字节。现在我们有一个400字节的缓存可以写入,那么写入日志的时候,我们就从缓存的末尾开始写入。比如存储第一条日志时,我们会从缓存的第350字节开始写入,所以350字节到400字节对应第一条日志,然后我们把当前可写的地址放在缓存的前8字节缓存。比如第一条日志写完后,下一个可写地址是350,所以我们把数据350存放在缓存的前8个字节。当我们要写入第二条日志时,我们读取缓存的前8个字节,得到值350。由于第二条缓存的长度为100字节,我们写入缓存的地址为350-100=250,所以我们写入post-buffer的250到350部分的内容对应第二条日志,然后我们将250写入缓存的前8个字节;写入第三条日志时,系统读取前8个字节得到值250,所以前8字节三个日志的写入地址为250-100=150,所以系统将第三条日志写入到缓冲区偏移量150字节,所以150字节到250字节的数据对应第三条日志,同时在前8字节存入150,以此类推。废话不多说,下面来看具体的代码实现。首先创建文件夹log_manager,添加log_manager.go,输入如下代码:use是先将写入的log存放在内存块中,一旦当前内存块满了,就会写入到一个磁盘文件中,然后创建一个新的内存块用于写入新日志。日志管理器每次启动时,都会根据给定的目录读取目录下的二进制文件,并将文件末尾的块读入内存,这样就可以获取到文件中存储的日志数据。packagelog_managerimport(fm"file_manager""sync")const(UINT64_LEN=8)typeLogManagerstruct{file_manager*fm.FileManagerlog_filestringlog_page*fm.Pagecurrent_blk*fm.BlockIdlatest_lsnuint64//当前日志序列号last_saved4lsn//uint6存储到磁盘的最后一个日志序列号=nil{returnnil,err}/*添加日志时,从内存底部往上走,例如内存为400字节,日志为100字节,那么日志将存储在300到400字节的内存,所以我们需要把当前内存可以写的前8个字节与底部偏移*/l.log_page.SetInt(0,uint64(l.file_manager.BlockSize()))l.file_manager.Write(&blk,l.log_page)return&blk,nil}funcNewLogManager(file_manager*fm.FileManager,log_filestring)(*LogManager,error){log_mgr:=LogManager{file_manager:file_manager,log_file:log_file,log_page:fm.NewPageBySize(file_manager.BlockSize()),last_saved_lsn:0,latest_lsn:0,}log_size,err:=file_manager.Size(log_file)iferr!=nil{returnnil,err}iflog_size==0{//如果文件是empty然后添加一个新的blockblk,err:=log_mgr.appendNewBlock()iferr!=nil{returnnil,err}log_mgr.current_blk=blk}else{//如果文件有数据,读取block末尾文件存入内存,最新的日志总是存放在文件的末尾}func(l*LogManager)FlushByLSN(lsnuint64)error{/*将给定的数字和之前的日志写入磁盘。注意与给定日志在同一个block的日志,即Page中的日志也会被写入磁盘例如,调用FlushLSN(65)表示将编号为65及之前的日志写入磁盘。如果编号为66和67的日志也和65在同一个Page,那么它们也会被写入磁盘*/iflsn>l.last_saved_lsn{err:=l.Flush()iferr!=nil{returnerr}l.last_saved_lsn=lsn}returnnil}func(l*LogManager)Flush()error{//将当前块数据写入disk_,err:=l.file_manager.Write(l.current_blk,l.log_page)iferr!=nil{returnerr}returnnil}func(l*LogManager)Append(log_record[]byte)(uint64,error){//添加日志l.mu.Lock()deferl.mu.Unlock()boundary:=l.log_page.GetInt(0)//获取可写底部偏移量record_size:=uint64(len(log_record))bytes_need:=record_size+UINT64_LENvarerrerrorifint(boundary-bytes_need)
