原文地址:https://blog.fengqingmo.top/articles/149
前言
WAL(Write-Ahead Logging)技术是一种常见的数据库事务日志技术,用于确保数据库的持久性和恢复能力。
核心思想是在对数据库进行任何修改之前,先将相应的日志记录写入到持久化的日志文件中。
bitcask 里数据和日志存放在一起,
基础概念
wal: 抽象为负责多个数据文件**(segment)**的管理器,
segment: 数据文件,分为 当前正在写的文件 和 已关闭的文件,每个文件内分为多个块
block: 一个文件被切分成多个固定大小的block
chunk: 实际存储的数据,每个键值对为一个chunk
需要实现的内容及问题
-
以上四个概念的抽象
-
wal与segment都需要实现其对应的迭代器
问题:
- 如果chunkSize > blockSize 该如何存储chunk,如果chunkSize 大于 SegmentSize 如何存储。
RoseDB Design
RoseDB数据的编码方案
编码方案指定了以何种格式如何存储连续/相关数据的字节
chunk
+----------+-------------+-----------+--- ... ---+
| CRC (4B) | Length (2B) | Type (1B) | Payload |
+----------+-------------+-----------+--- ... ---+
CRC = 32-bit hash computed over the payload using CRC
Length = Length of the payload data
Type = Type of record
(FullType, FirstType, MiddleType, LastType)
The type is used to group a bunch of records together to represent
blocks that are larger than BlockSize
Payload = Byte stream as long as specified by the payload size
格式的数据类型实现
// ChunkPosition represents the position of a chunk in a segment file.
// Used to read the data from the segment file.
type ChunkPosition struct {
// 段文件编号
SegmentId SegmentID
// 在第几个 block 内
BlockNumber uint32
// ChunkOffset The start offset of the chunk in the segment file.
ChunkOffset int64
// ChunkSize How many bytes the chunk data takes up in the segment file.
ChunkSize uint32
}
写入一个chunk
- 先判断当前 block剩下空间能否放下一个 chunk header, 不可以则将block剩余空间填满,新开一个block写
- 判断整个chunk(dataSize + chunkHeaderSize)能否放到当前block剩下的空间内,可以则直接放入,chunkType为full
- 否则继续写入,通过chunk剩下需要被写的大小判断当前chunk的类型
segment:
+-----+-------------+--+----+----------+------+-- ... ----+
File | r0 | r1 |P | r2 | r3 | r4 | |
+-----+-------------+--+----+----------+------+-- ... ----+
|<---- BlockSize ----->|<---- BlockSize ----->|
rn = variable size records
P = Padding
BlockSize = 32KB
格式的数据类型实现
// Segment represents a single segment file in WAL.
// The segment file is append-only, and the data is written in blocks.
// Each block is 32KB, and the data is written in chunks.
type segment struct {
id SegmentID
fd *os.File
//当前在第几个block/已使用的block数量
currentBlockNumber uint32
//当前block已使用的大小
currentBlockSize uint32
closed bool
header []byte
startupBlock *startupBlock
isStartupTraversal bool
}
new Segment
segmentId 是 自增的
// openSegmentFile a new segment file.
func openSegmentFile(dirPath, extName string, id uint32) (*segment, error) {
fd, err := os.OpenFile(
SegmentFileName(dirPath, extName, id),
os.O_CREATE|os.O_RDWR|os.O_APPEND,
fileModePerm,
)
if err != nil {
return nil, err
}
// set the current block number and block size.
offset, err := fd.Seek(0, io.SeekEnd)
if err != nil {
return nil, fmt.Errorf("seek to the end of segment file %d%s failed: %v", id, extName, err)
}
return &segment{
id: id,
fd: fd,
header: make([]byte, chunkHeaderSize),
currentBlockNumber: uint32(offset / blockSize),
currentBlockSize: uint32(offset % blockSize),
startupBlock: &startupBlock{
block: make([]byte, blockSize),
blockNumber: -1,
},
isStartupTraversal: false,
}, nil
}
Segment Reader
用于遍历文件的chunk
// segmentReader is used to iterate all the data from the segment file.
// You can call Next to get the next chunk data,
// and io.EOF will be returned when there is no data.
type segmentReader struct {
segment *segment
//段文件含有的block数量
blockNumber uint32
// 当前chunk在文件内的偏移
chunkOffset int64
}
重要的方法
*func (seg segment)readInternal(blockNumber uint32, chunkOffset int64)… : 从段文件中读取数据。
func (segReader *segmentReader) Next() ([]byte, *ChunkPosition, error): 获取seg的下一个chunk
WAL:用来管理所有文件
数据类型实现
// WAL 代表一个预写日志结构,提供数据的持久性和容错能力。
// 它包含活跃段(当前用于新写入的段文件)和旧段(用于读操作的旧段文件)。
// 选项字段存储各种配置选项。
// 互斥锁(mu)用于并发访问WAL数据结构,确保安全访问和修改。
type WAL struct {
// 活跃段文件,用于新的写入数据。
activeSegment *segment
// 旧段文件,仅用于读取。
olderSegments map[SegmentID]*segment
// 配置选项。
options Options
// 用于并发访问和修改的互斥锁。
mu sync.RWMutex
// 写入的字节数。
bytesWrite uint32
// 重命名ID列表。
renameIds []SegmentID
// 等待写入的数据数组。
pendingWrites [][]byte
// 等待写入的大小。
pendingSize int64
// 等待写入的锁。
pendingWritesLock sync.Mutex
}
new wal:加载所有文件
读取目录下的所有文件,segmentId 最大的为当前活跃文件
*func Open(options Options) (WAL, error)
WAL Reader
负责迭代所有segment
// Reader represents a reader for the WAL.
// It consists of segmentReaders, which is a slice of segmentReader
// structures sorted by segment id,
// and currentReader, which is the index of the current segmentReader in the slice.
//
// The currentReader field is used to iterate over the segmentReaders slice.
type Reader struct {
segmentReaders []*segmentReader
currentReader int
}
总结
写好底层的数据结构需要很强的编码能力和抽象能力。