2.3 持久化命令行

栏目: 数据库 · 发布时间: 6年前

内容简介:直到现在,我们的区块链实现中还没有用到数据库,我们只是把每次启动程序计算得到的区块储存在内存中。我们不能复用一个之前生成的区块链,也不能与他人分享,因此,现在我们要把它存在磁盘上。那该选择什么样的数据库?其实任何一种都可以。在比特币文档中,没有说要一个具体的数据库,所以这取决于开发者。Bitcoin Core用的是LevelDB。本篇教程中使用BoltDB。BoltDB有如下特性:

数据库选型

直到现在,我们的区块链实现中还没有用到数据库,我们只是把每次启动程序计算得到的区块储存在内存中。我们不能复用一个之前生成的区块链,也不能与他人分享,因此,现在我们要把它存在磁盘上。 

那该选择什么样的数据库?其实任何一种都可以。在比特币文档中,没有说要一个具体的数据库,所以这取决于开发者。Bitcoin Core用的是LevelDB。本篇教程中使用BoltDB。

BoltDB

BoltDB有如下特性: 

1. 小而简约 

2. 使用 Go 实现 

3. 不需要单独部署 

4. 支持我们的数据结构 

它的Github中这样描述 

Bolt is a pure Go key/value store inspired by Howard Chu’s LMDB project. The goal of the project is to provide a simple,fast, and reliable database for projects that don’t require a full database server such as Postgres or MySQL. 

Bolt受Howard Chu的LMDB项目启发,纯Golang编写的key/value数据库。应运只需要简单、快速、可靠,不需要全数据库(如Mysql)功能的项目而生。 

Since Bolt is meant to be used as such a low-level piece of functionality, simplicity is key. The API will be small and only focus on getting values and setting values. That’s it. 

使用Bolt意味着只需要用到很少的(数据库)功能,所以足够简单是关键。而它的API只专注于值的读写。 

是吧,我们只要这些功能。再稍稍多赘述一点它的信息。 

BoltDB是基于key/value存储,即是没有像 SQL 关系性数据库(MySQL、PG)那样的的表,也没有行、列。而数据只存在于Key-value结构中(和Golang的maps很像)。Key-value存放在和SQL的表功能差不多的桶(buckets)中,所以要得到值,就得知道“桶”和“key”。 

还有一点比较重要的是,BoltDB是没有数据类型的,key和value都是byte型的数组。因为我们要存储Golang的结构体(比如Block),所以会把这些结构体序列化。我们会使用encoding/gob来序列/解序列化结构体,当然也可以使用 JSON、XML、Protocol Buffers等方案,使用它主要是简单,而且它也是Golang库标准的一部分。

数据结构

在实现持久化之前,我们得先搞清楚要怎么存储,先看看Bitcoin Core是怎么搞的。 

简单而言,Bitcoin Core用了两个“buckets”来储存数据: 

1. blocks 存储了该链中所有的区块的元数据 

2. chainstate 存储链的状态,储存当前未完成的事务信息及其它一些元数据。 

各区块是存储在磁盘上独立的文件当中。这么做的机制是为了保证读取一个区块不会加载所有(或部分)区块到内存中。这个特性我们现在也不去实现它。 

在 blocks 中,key->value对有: 

1. ‘b’ + 32-byte block hash -> block index record 

2. ‘f’ + 4-byte file number -> file information record 

3. ‘l’ -> 4-byte file number: the last block file number used 

4. ‘R’ -> 1-byte boolean: whether we’re in the process of reindexing 

5. ‘F’ + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off 

6. ‘t’ + 32-byte transaction hash -> transaction index record 

翻译一下 

1. ‘b’ + 32-byte 该块的hash码 -> 块索引记录 

2. ‘f’ + 4-byte 文件编号 -> 文件信息记录 

3. ‘l’ -> 4-byte 文件编号: 最后一块文件的编号 

4. ‘R’ -> 1-byte 布尔值: 标记是否正在重置索引 

5. ‘F’ + 1-byte 标记名长度 + 标记名 -> 1 byte boolean: 各种可关可开的标记 

6. ‘t’ + 32-byte 交易的hash值 -> 交易的索引记录 

在 chainstate, key->value对有: 

1. ‘c’ + 32-byte transaction hash -> unspent transaction output record for that transaction 

2. ‘B’ -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs 

翻译一下 

1. ‘c’ + 32-byte 交易的hash值 -> 未完成的交易记录 

2. ‘B’ -> 32-byte 块hash值: 数据库记录的未使用的交易的output的块hash

因为我们现在还没有交易,所以暂时只有 Blocks,还有就是现在我们不把区块各自存在独立的文件中,而把整个DB当作一个文件存储Blocks。所以我们不需要任何关联到文件的数字。 

所以,Blocks就简化成这样: 

1. 32-byte block-hash -> Block structure (serialized) 

2. ‘l’ -> the hash of the last block in a chain 

下面开始实现持久化机制

序列化

由于BoltDB只能存储byte数组,所以先给Block实现序列化方法。

func (b *Block) Serialize() []byte { 
var result bytes.Buffer 
… 
encoder := gob.NewEncoder(&result) 
err := encoder.Encode(b) 
… 
return result.Bytes() 
} 

再实现解序列化方法 

func DeserializeBlock(d []byte) *Block { 
var block Block 
… 
decoder := gob.NewDecoder(bytes.NewReader(d)) 
err := decoder.Decode(█) 
… 
return █ 
}

持久化

我们先从优化 NewBlockchain 方法开始。之前这个方法只能创建新的区块链再增加创世区块到链中。现在它加上以下这些能力: 

1. 打开DB文件 

2. 检测是否已经有区块链存在 

3. 如果存在 

1. 创建新区块链实例 

2. 把刚建的这个区块链信息的作为最后一块区块hash塞到DB中。 

4. 如果不存在 

1. 创建新的创世区块 

2. 存储到DB中 

3. 把创世区块的hash作为末端hash 

4. 创建新的区块链,把它的信息指向创世区块 

转化为代码:

func NewBlockchain() *Blockchain {
        var tip []byte
db, err := bolt.Open(dbFile, 0600, nil)
    ...
        err = db.Update(func(tx *bolt.Tx) error {
                b := tx.Bucket([]byte(blocksBucket))
if b == nil {
                        genesis := NewGenesisBlock()
                        b, err := tx.CreateBucket([]byte(blocksBucket))
                        err = b.Put(genesis.Hash, genesis.Serialize())
                        err = b.Put([]byte("l"), genesis.Hash)
                        tip = genesis.Hash
                } else {
                        tip = b.Get([]byte("l"))
                }
return nil
        })
bc := Blockchain{tip, db}
return &bc
}

分析一下代码 

db, err := bolt.Open(dbFile, 0600, nil) 

这是打开BoltDB数据库文件的标准方式,切记:即使没有找到文件,也不会返回错误 

err = db.Update(func(tx *bolt.Tx) error { 

… 

}) 

操作BoltDB需要使用一个参数为事务的回调函数。这里的事务有两种类型–read-only,read-write。因为我们会把创世区块放到DB中,所以我们使用read-write的事务,也就是db.Update(…)

b := tx.Bucket([]byte(blocksBucket))
if b == nil {
        genesis := NewGenesisBlock()
        b, err := tx.CreateBucket([]byte(blocksBucket))
        err = b.Put(genesis.Hash, genesis.Serialize())
        err = b.Put([]byte("l"), genesis.Hash)
        tip = genesis.Hash
} else {
        tip = b.Get([]byte("l"))
}

这一段是核心,先获取一个Bucket用来存储区块:如果桶存在,那么读取 l值;如果不存在,则创建创世区块,再创建桶,然后把块扔到桶里,把块的hash值设为 l 值。 

还有注意新建区块链的方式: 

bc := Blockchain{tip, db} 

这里不再把所有的区块放到区块链中,而是只设置区块的提示信息和db的连接(因为在整个程序运行时,区块链会一直保持与数据库的连接)。所以,区块链的结构会被改成:

type Blockchain struct {
        tip []byte
        db  *bolt.DB
}

下一步是修改 AddBlock方法,增加新的区块不再像之前直接把数据传过去那么简单了,现在要把区块存储到db中:

func (bc *Blockchain) AddBlock(data string) {
        var lastHash []byte
err := bc.db.View(func(tx *bolt.Tx) error {
                b := tx.Bucket([]byte(blocksBucket))
                lastHash = b.Get([]byte("l"))
return nil
        })
newBlock := NewBlock(data, lastHash)
err = bc.db.Update(func(tx *bolt.Tx) error {
                b := tx.Bucket([]byte(blocksBucket))
                err := b.Put(newBlock.Hash, newBlock.Serialize())
                err = b.Put([]byte("l"), newBlock.Hash)
                bc.tip = newBlock.Hash
return nil
        })
}

逐段分析一下:

err := bc.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        lastHash = b.Get([]byte("l"))
return nil
})

这里使用的是 read-only事务的 Get 方法,从l中读取最后一块区块的编码,我们挖下一新块时会作为参数用到。

newBlock := NewBlock(data, lastHash)
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash

在挖出新块,将其序列化存储到数据库后,把最新的区块hash值更新到 l 值中。 

检查区块 

到这一步,区块都保存到数据库了,现在可以把区块链重新加载然后把新块加到里面。但是现在不能再打印区块链中的区块了,因为已经不是把区块保存在数组中了。现在修复这个缺陷。 

BoltDB支持遍历一个桶中的所有key,但是这些key都是基于byte-sorted顺序 排序 的,而我们需要让它们按在区块中的顺序打印出来,我们也不加载所有的区块到内存中(区块可能会很大,没有必要加载完,或者,假装加载完了),先一个一个读取。现在需要一个blockchain的遍历器:

type BlockchainIterator struct {
        currentHash []byte
        db          *bolt.DB
}

在每次我们要去遍历整个区块链中的区块时会创建一个该遍历器。遍历器会保存当前遍历到的区块hash和保持与数据库的链接,后者也使得遍历器和该区块链在逻辑上是结合的,因为遍历器数据库连接用的是区块链的同一个,所以,Blockchain 会负责创建遍历器:

func (bc *Blockchain) Iterator() *BlockchainIterator {
        bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}

注意遍历器用区块链的顶端tip初始化,因此,区块是从顶端到末端,也就是从最老的区块到最新区块。事实上,选择这个tip意味着给区块链“投票”。一个区块链会有很多分支,而最长的那支会被认为是主分支。在获致到tip(可以是该区块链中的任何一个区块)之后,就可以重建整个区块链,算出它的长度和重建这个区块的工作量。所以,tip也可以认为是区块链的一个标识符。 

BlockchainIterator 只做一件事:它负责返回区块链中的下一个区块:

func (i *BlockchainIterator) Next() *Block {
        var block *Block
err := i.db.View(func(tx *bolt.Tx) error {
                b := tx.Bucket([]byte(blocksBucket))
                encodedBlock := b.Get(i.currentHash)
                block = DeserializeBlock(encodedBlock)
return nil
        })
i.currentHash = block.PrevBlockHash
return block
}

2.3 持久化命令行


以上所述就是小编给大家介绍的《2.3 持久化命令行》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

UML精粹:标准对象建模语言简明指南(第3版)(英文影印版)

UML精粹:标准对象建模语言简明指南(第3版)(英文影印版)

福勒 / 清华大学出版社 / 2006年3月1日 / 26.00元

《UML精粹:标准对象建模语言简明指南》(影印版)(第3版)可作为高等学校计算机、电子、通信等专业高年级学生及研究生课程之教学用书,同时对软件研究者与开发人员亦颇具参考价值。一起来看看 《UML精粹:标准对象建模语言简明指南(第3版)(英文影印版)》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试