2.3 持久化命令列
資料庫選型
直到現在,我們的區塊鏈實現中還沒有用到資料庫,我們只是把每次啟動程式計算得到的區塊儲存在記憶體中。我們不能複用一個之前生成的區塊鏈,也不能與他人分享,因此,現在我們要把它存在磁碟上。
那該選擇什麼樣的資料庫?其實任何一種都可以。在比特幣文件中,沒有說要一個具體的資料庫,所以這取決於開發者。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(&block)
…
return &block
}
持久化
我們先從優化 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
}
-
學院Go語言視訊主頁
https://edu.csdn.net/lecturer/1928 -
掃碼獲取海量視訊及原始碼 QQ群:721929980
相關文章
- 基於Java語言構建區塊鏈(三)—— 持久化 & 命令列Java區塊鏈持久化命令列
- Redis資料持久化—RDB持久化與AOF持久化Redis持久化
- AOF持久化(儲存的是操作redis命令)持久化Redis
- Redis的兩種持久化方式-快照持久化(RDB)和AOF持久化Redis持久化
- redis系列:RDB持久化與AOF持久化Redis持久化
- Redis 持久化Redis持久化
- Redis - 持久化Redis持久化
- redisaof持久化Redis持久化
- ehcache持久化持久化
- Docker 持久化Docker持久化
- redis持久化Redis持久化
- [Redis]持久化Redis持久化
- SpringCloud使用Sentinel,Sentinel持久化,Sentinel使用nacos持久化SpringGCCloud持久化
- redis ——AOF持久化Redis持久化
- Redis 持久化方案Redis持久化
- redis 之 持久化Redis持久化
- Redis:持久化篇Redis持久化
- Redis 的持久化Redis持久化
- Redis 持久化(Persistence)Redis持久化
- redis-持久化Redis持久化
- 可持久化trie持久化
- Redis 七 持久化Redis持久化
- Redis的持久化Redis持久化
- redis持久化策略Redis持久化
- OpenCV持久化(一)OpenCV持久化
- OpenCV持久化(二)OpenCV持久化
- Redis之持久化Redis持久化
- ActiveMQ持久化方式MQ持久化
- redis 持久化策略Redis持久化
- 巧用Startup簡化Java命令列程式Java命令列
- RDD持久化,不使用RDD持久化的問題的工作原理持久化
- Redis的持久化方案Redis持久化
- 使用 Java 持久化 APIJava持久化API
- Redis 持久化詳解Redis持久化
- redis快照--RDB持久化Redis持久化
- Redis的持久化方式Redis持久化
- fabric資料持久化持久化
- vuex持久化方案探究Vue持久化