通过前面的文章,我们知道了有工作量证明的区块链是怎么构建的,然而区块链一直在内存中当然是不行的,我们需要将区块链持久化到数据库中。
区块链的本质是一个分布式数据库,尽管如此,底层使用的数据库并没有太大要求,不需要分布式,分片等等特性,这些特性是在数据库的上层做的。所有的节点都是一个完整的实例,是它们组成了一个分布式系统。就实现业务来说,我们只需要一个简单的 k-v 数据库就够了。
geth 采用的是 LevelDB,回头来看这个选择还是有很多可以反思的地方,LevelDB 在顺序读写的场景下性能很好,但 geth 在运行智能合约时,面临的更多的场景是大量时间耗费在世界状态的读写上,通过前面对 MPT 的分析我们知道,这些 IO 大多是离散随机的,这导致了 geth 的性能瓶颈,特别是在合约里的数据大量分布在 Storage Trie 的情况下,因为树的高度可能很高,而每加载一个节点都需要进行一次 IO。这一点在以太坊(Ethereum) 的执行交易性能瓶颈这篇文章中有论述。邓草原 正在开发的 Kesque 也可以关注一下。这不是本文重点,就不展开了。
中本聪的最初论文并没有提具体要用什么数据库,尽管如此,社区使用最多的参考实现 Bitcoin Core 最初使用的是 Berkeley DB,后来转而使用 levelDB 存区块链索引,以太坊的 Go,C++, Python 实现的客户端使用的是 LevelDB 数据库,Rust 客户端 Parity 使用的是 RocksDB。
go-ethereum 对 LevelDB 的增删改查是通过 ethdb 这个模块来交互的。ethdb 实际上是对 github.com/syndtr/goleveldb/leveldb 这个库的封装,其中增加了收集 metric 的功能。
如果想要对 LevelDB 有更深层次的了解可以自行查询相关资料。
ethdb 模块的功能比较单一,就4个文件:
- interface.go 定义数据库增删改查的接口
- database.go 封装 levelDB 的代码
- memory_database.go 基于内存的数据库,不会持久化到文件,只在测试时使用
- database_test.go 测试用例
interface.go
1 | type Database interface { |
Database 接口定义了所有数据库操作,Putter 接口定义批量操作和普通操作的接口。批量操作不能多线程同时使用。
memory_database.go
1 | type MemDatabase struct { |
这个文件跟 LevelDB 没什么关系,它只是封装了内存里的 key-value 结构,暴露的增删改查的接口(Put,Has,Get,Keys,Delete 等等)跟对 LevelDB 的封装基本一致。加了锁以在多线程的情况下对资源进行保护。
database.go
封装了 levelDB 接口,在 go-ethereum 中大量使用。
1 | type LDBDatabase struct { |
Path, Put, Has, Get 等方法是对 levelDB 的封装,没有什么特别的,也不需要用锁,因为 github.com/syndtr/goleveldb/leveldb 支持多线程访问。
Path
1 | func (db *LDBDatabase) Path() string { |
Put
1 | func (db *LDBDatabase) Put(key []byte, value []byte) error { |
Has
1 | func (db *LDBDatabase) Has(key []byte) (bool, error) { |
Get
1 | func (db *LDBDatabase) Get(key []byte) ([]byte, error) { |
Delete
1 | func (db *LDBDatabase) Delete(key []byte) error { |
Metrics
compTimeMeter
, compReadMeter
, compWriteMeter
, diskReadMeter
, diskWriteMeter
这几个方法都通过 metrics.NewRegisteredMeter
注册得到。
1 | func (db *LDBDatabase) Meter(prefix string) { |
最后通过一个 goroutine 调用 db.meter。
该方法是一个死循环,过三秒钟就通过 db.db.GetProperty("leveldb.iostats")
获取一次 leveldb 的 stats,然后发到 metrics 系统。如果收到 quitChan 的信号就退出死循环。
业务相关代码
与业务直接相关的代码在 core/rawdb
目录下,基本上所有与底层数据库交互的代码都在这里,没有很复杂的内容,不过了解这些底层的数据结构对于理解业务很有帮助,可以自行浏览。
key->value
以下是一些 geth 中常见的 key-value 对(在 core/rawdb/schema.go
中定义)。
key | value | 描述 |
---|---|---|
“h” + num + hash | 区块头的数据 | 根据区块高度和区块哈希找到区块头数据(RLP 编码) |
“h” + num + hash + “t” | 区块总难度 | 根据区块高度,区块哈希找到 totalDifficulty,即累计的区块难度的和 |
“h” + num + “n” | 区块头哈希值 | 根据存储的规范区块链高度找到区块头哈希值 |
“H” + hash | 区块高度 | 根据哈希值找到区块高度 |
“b” + num + hash | 区块体的数据 | 根据区块高度,区块哈希找到区块体的数据(RLP 编码) |
“r” + num + hash | 收据的数据 | 根据区块高度,区块哈希找到收据的数据(RLP 编码) |
“l” + hash | 交易,收据查找的 metadata | 根据哈希值找到交易,收据查找的 metadata |
“B” + bit + section + hash | 布隆过滤器的结果 | 用于日志的过滤 |