2.3 持久化命令行

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

内容简介:直到现在,我们的区块链实现中还没有用到数据库,我们只是把每次启动程序计算得到的区块储存在内存中。我们不能复用一个之前生成的区块链,也不能与他人分享,因此,现在我们要把它存在磁盘上。那该选择什么样的数据库?其实任何一种都可以。在比特币文档中,没有说要一个具体的数据库,所以这取决于开发者。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 持久化命令行》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

The Creative Curve

The Creative Curve

Allen Gannett / Knopf Doubleday Publishing Group / 2018-6-12

Big data entrepreneur Allen Gannett overturns the mythology around creative genius, and reveals the science and secrets behind achieving breakout commercial success in any field. We have been s......一起来看看 《The Creative Curve》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具