我们继续上篇的bolt源码阅读。

文件锁

数据库由于有读写操作,所以会在一定情况下会有多个进程同时写一个文件的情况。

1
2
3
4
if err := flock(db, mode, !db.readOnly, options.Timeout); err != nil {
		_ = db.close()
		return nil, err
}

bolt使用系统的文件锁来防止多个进程同时对一个文件操作。这样就进一步防止了用户创建了两个不同的实例对一个数据库操作导致的问题的发生。 写入函数很简单,直接使用

1
2
// Default values for test hooks
db.ops.writeAt = db.file.WriteAt

系统默认的写入方式写入文件。

文件判断

通过使用了上述的OpenFile函数,系统中可能已经存在数据,也有可能没有存在,因此需要进行判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if info, err := db.file.Stat(); err != nil {
	return nil, err
} else if info.Size() == 0 {
	// Initialize new files with meta pages.
	if err := db.init(); err != nil {
		return nil, err
	}
} else {
	// Read the first meta page to determine the page size.
	var buf [0x1000]byte
	if _, err := db.file.ReadAt(buf[:], 0); err == nil {
		m := db.pageInBuffer(buf[:], 0).meta()
		if err := m.validate(); err != nil {
			db.pageSize = os.Getpagesize()
		} else {
			db.pageSize = int(m.pageSize)
		}
	}
}

如果连文件信息都无法获取,则直接认定出错,无需进行后续的处理。如果文件大小为0,代表是一个新的文件,需要对数据库进行初始化。否则的话需要读取数据库文件。

初始化

为了方便从头理解,我们就从初始化一个数据库开始读起。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func (db *DB) init() error {
	// Set the page size to the OS page size.
	db.pageSize = os.Getpagesize()

	// Create two meta pages on a buffer.
	buf := make([]byte, db.pageSize*4)
	
	...
	...

	return nil
}

初始化函数多个几个我们刚刚没有看到的内容,我们一步一步来分析。 首先,bolt使用mmap进行内存与文件系统的映射转换,因此,在一个操作系统中,需要知道给定的分页大小。我们假设系统给定的分页大小是4096。 首先创建了4*4096长度的字节数组。然后每次都往buf中写入数据,每次写入的都是4096(pageSize)大小。

page

在这个地方,出现了第一个我们需要认识的数据类型page。 我们先在这停一下,来了解下page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const pageHeaderSize = int(unsafe.Offsetof(((*page)(nil)).ptr))

const branchPageElementSize = int(unsafe.Sizeof(branchPageElement{}))
const leafPageElementSize = int(unsafe.Sizeof(leafPageElement{}))

const (
	branchPageFlag   = 0x01
	leafPageFlag     = 0x02
	metaPageFlag     = 0x04
	freelistPageFlag = 0x10
)

type pgid uint64

type page struct {
	id       pgid
	flags    uint16
	count    uint16
	overflow uint32
	ptr      uintptr
}

这里面,id就是指每一个页面的Id flags是表示页面的数据类型,包含branchPageFlagleafPageFlag等四种。 其次是count,也就是页面内存储元素的个数,包括branchPageElementleafPageElement。 接下来的overflow字面意思是溢出,就是指页面是否有超出了pageSize的大小,需要进行连接。 ptr是指的数据element开始的地址。 我们画个示意图了解一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
              +---------------+
              |   Id          |
              |               |
              +---------------+
              |    Flags      |
              |               |
              |               |
              +---------------+
              |     Count     |
              |               |
              +---------------+
              |      overflow |
              |               |
ptr+--------> +---------------+
              |               |
              |               |
              |     Elements  |
              |               |
              |               |
              |               |
              |               |
              |               |
              |               |
              |               |
              +---------------+

这就是一个页面的结构。 为什么ptr不再内存中呢?

1
const pageHeaderSize = int(unsafe.Offsetof(((*page)(nil)).ptr))

可以看出,pageHeaderSize到ptr就结束了。所以ptr仅仅是一个分界线而已。

meta

除此之外,源码中还有一个meta结构,我们一并分析下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type meta struct {
	magic    uint32
	version  uint32
	pageSize uint32
	flags    uint32
	root     bucket
	freelist pgid
	pgid     pgid
	txid     txid
	checksum uint64
}

首先magic,就是一个表名是bolt的数字,没有太多的意义。 version,版本号。 pageSize在我们的文件判断中有段代码是db.pageSize = int(m.pageSize)就是代表的页面大小。 flags没有看到如何使用。 root是数据库的一个根bucket freelist,就是指的空闲页面的标号 pgid,记录的是数据库一共存了多少页 txid,交易Id,表示数据库当前进行的版本。 checksum,校验和,确保以后读取的信息没有错误。

配置

现在我们继续回到初始化,可以看到,程序首先将前两个page设置为metaPage,其中meta的配置如下

1
2
3
4
5
6
7
m.magic = magic
m.version = version
m.pageSize = uint32(db.pageSize)
m.freelist = 2
m.root = bucket{root: 3}
m.pgid = 4
m.txid = txid(i)

我们主要关注freelistpgid。 可以看到freelist设置为2,也就是我们接下来要设置的freelist页。 pgid设置为4,因为我们再创建buf数组时制定的大小是4*pageSize,所以一共有四个page。 接下来bolt配置page3(freelist页)和page4(leafPage页)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Write an empty freelist at page 3.
p := db.pageInBuffer(buf[:], pgid(2))
p.id = pgid(2)
p.flags = freelistPageFlag
p.count = 0

// Write an empty leaf page at page 4.
p = db.pageInBuffer(buf[:], pgid(3))
p.id = pgid(3)
p.flags = leafPageFlag
p.count = 0

没有什么难以理解的地方。 然后系统将buf写入文件中。

1
2
3
4
5
6
if _, err := db.ops.writeAt(buf, 0); err != nil {
	return err
}
if err := fdatasync(db); err != nil {
	return err
}

初始化结束。

参考文献

区块的持久化之BoltDB(一)