1. boltdb数据库db.Opne()代码流程分析

根据bolt db 的官方文档,我们可以看到初始化数据库只需要一行代码。

The top-level object in Bolt is a DB. It is represented as a single file on your disk and represents a consistent snapshot of your data.
To open your database, simply use the bolt.Open() function:

// Open the my.db data file in your current directory.
// It will be created if it doesn't exist.
db, err := bolt.Open("my.db", 0600, nil)

在db.go中定义了该函数

// Open creates and opens a database at the given path.
// If the file does not exist then it will be created automatically.
// Passing in nil options will cause Bolt to open the database with the default options.
func Open(path string, mode os.FileMode, options *Options) (*DB, error) {}

首先我们对该函数的执行流程进行探讨,该函数首先将配置参数加载到DB结构体中。

var db = &DB{opened: true}

	// Set default options if no options are provided.
	if options == nil {
		options = DefaultOptions
	}
	db.NoGrowSync = options.NoGrowSync
	db.MmapFlags = options.MmapFlags

	// Set default values for later DB operations.
	db.MaxBatchSize = DefaultMaxBatchSize
	db.MaxBatchDelay = DefaultMaxBatchDelay
	db.AllocSize = DefaultAllocSize

	flag := os.O_RDWR
	if options.ReadOnly {
		flag = os.O_RDONLY
		db.readOnly = true
	}

然后使用 os.OpenFile 获取文件句柄。
使用bolt_windows.go中的flock函数,创建一个单独的锁文件,对文件进行加锁检查操作,另外该函数还实现了超时检查。
接下来的代码是为了检验数据库文件是否存在,进而加载元数据或新建数据库文件。

// Initialize the database if it doesn't exist.
	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 {
		// 阅读第一个元页面以确定页面大小。
		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)
			}
		}
	}

1.1. 新建流程

其中 db.init( ) 函数是用来初始化数据库文件。该函数首先申请了四个页面(物理内存分配的最小单位)的内存,
通过pageInBuffer获取page结构指针以便操作。

 func (db *DB) pageInBuffer(b []byte, id pgid) *page {
	// 使用unsafe.Pointer以避免类型检查
	return (*page)(unsafe.Pointer(&b[id*pgid(db.pageSize)]))
}

page的定义在page.go中,在page中存有五个无符号整形变量。

type page struct {
	id       pgid
	flags    uint16
	count    uint16
	overflow uint32
	// 由于uintptr是一个整数类型,uintptr值可以进行算术运算。 所以通过使用uintptr和unsafe.Pointer,
	// 我们可以绕过限制—* T值不能在Golang中计算偏移量
	ptr      uintptr
}

其中:

  1. id:是pgid类型,是给page的编号。
  2. flags:是指的此页中保存的具体数据类型。(有好几种)
  3. count:记录具体数据类型中的计数,不同的类型具有不同的含义
  4. overflow:用来记录是否有跨页
  5. ptr:是具体的数据类型。

在page结构体内的ptr指针在metaPage中具体指向一个meta类型,meta的初始化代码如下:

        // Initialize the meta page.
		m := p.meta()
		m.magic = magic  // 表示为bolt文件
		m.version = version // 版本号
		m.pageSize = uint32(db.pageSize)
		m.freelist = 2 // freelist的pgid
		m.root = bucket{root: 3} // 指向bucket的pgid
		m.pgid = 4 // 正式数据是从pgid4开始的
		m.txid = txid(i) // 事务序列号
		m.checksum = m.sum64()

之后,还会依次初始化两个页面。

    // 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

至此,db.Opne( )依次创建了四个页面,分别是 meta、meta、freelist、leafPageFlag(后两个页面的用途以后讨论)。
然后将buf写入文件,并刷新到磁盘上。

1.2. 读取流程

读取时,db.Open( )会打开数据库文件并读取文件前[0x1000]bytes以获取meta信息,从中得到pagesize。如果获取失败,db会尝试使用系统的pagesize作为db.pageSize。
之后db会初始化一个对象资源池,以减少GC提升性能。

    // Initialize page pool.
	db.pagePool = sync.Pool{
		New: func() interface{} {
			return make([]byte, db.pageSize)
		},
	}

接下来db调用mmap()函数,该函数被用来将数据文件映射到虚拟内存中。其首先使用syscall.CreateFileMapping创建共享内存,然后使用 syscall.MapViewOfFile 函数,该函数使用CreateFileMapping返回的文件映射对象句柄来在进程的虚拟地址空间里建立文件的视图。并使用db.data指向这段内存地址。最后会检查两个meta页面的字节和版本,以确保它符合要求且具有可用性。值得一提的是,mmap函数使用了mmaplock以保证不会重入。

db还会初始化freelist成员变量通过读取文件中的freelist页面,建立可用和待处理page缓存。freelist表示可用于分配的所有页面的列表。它还跟踪已释放但仍被开放事务使用的页面。

最终,db.Open完成了从磁盘读取或创建数据库文件的流程,并建立了页面跟踪缓存。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐