Golang實(shí)現(xiàn)事務(wù)型內(nèi)存數(shù)據(jù)庫(kù)的方法詳解
內(nèi)存數(shù)據(jù)庫(kù)經(jīng)我們經(jīng)常用到,例如Redis,那么如何從零實(shí)現(xiàn)一個(gè)內(nèi)存數(shù)據(jù)庫(kù)呢,本文旨在介紹如何使用Golang編寫(xiě)一個(gè)KV內(nèi)存數(shù)據(jù)庫(kù)MossDB。
特性
MossDB是一個(gè)純Golang編寫(xiě)、可嵌入的、鍵值型內(nèi)存數(shù)據(jù)庫(kù),包含以下特性
- 可持久化,類(lèi)似Redis AOF(Append only Log)
- 支持事務(wù)
- 支持近實(shí)時(shí)的TTL(Time to Live), 可以實(shí)現(xiàn)毫秒級(jí)的過(guò)期刪除
- 前綴搜索
- Watch接口,可以監(jiān)聽(tīng)某個(gè)鍵值的內(nèi)容變化,類(lèi)似etcd的Watch
- 多后端存儲(chǔ),目前支持HashMap和RadixTree
命名由來(lái)
Moss有苔、苔花的含義,MossDB的名字來(lái)源于清代袁牧的一句詩(shī):
苔花如米小,也學(xué)牡丹開(kāi)
MossDB雖小,但五臟俱全,也支持了很多重要功能。另外,巧合的是《流浪地球2》中的超級(jí)計(jì)算機(jī)550W名字就是Moss。
架構(gòu)
內(nèi)存數(shù)據(jù)庫(kù)雖然使用簡(jiǎn)單,實(shí)現(xiàn)起來(lái)卻有很多細(xì)節(jié),Golang目前也存在不少優(yōu)秀的開(kāi)源內(nèi)存數(shù)據(jù)庫(kù),比如buntdb、go-memdb,在編寫(xiě)MossDB過(guò)程中也借鑒了一些它們的特性。
MossDB的架構(gòu)如圖:

自上往下分為:
- 接口層,提供API接受用戶(hù)請(qǐng)求
- 核心層,實(shí)現(xiàn)事務(wù)、過(guò)期刪除、Watch等功能
- 存儲(chǔ)層,提供KV的后端存儲(chǔ)以及增刪改查
- 持久化層,使用AOL持久化即每次修改操作都會(huì)持久化到磁盤(pán)Log中
快速開(kāi)始
MossDB可嵌入到Go程序中,可以通過(guò)go get獲取
go get github.com/qingwave/mossdb
MossDB提供了易用的API,可以方便地進(jìn)行數(shù)據(jù)處理,下面的示例代碼展示了如何使用MossDB:
package main
import (
"log"
"github.com/qingwave/mossdb"
)
func main() {
// create db instance
db, err := mossdb.New(&mossdb.Config{})
if err != nil {
log.Fatal(err)
}
// set, get data
db.Set("key1", []byte("val1"))
log.Printf("get key1: %s", db.Get("key1"))
// transaction
db.Tx(func(tx *mossdb.Tx) error {
val1 := tx.Get("key1")
tx.Set("key2", val1)
return nil
})
}更多示例見(jiàn)源碼
具體實(shí)現(xiàn)
從下往上分別介紹下MossDB如何設(shè)計(jì)與實(shí)現(xiàn),以及相關(guān)的細(xì)節(jié)。
AOF持久化
AOF源于Redis提供兩種持久化技術(shù),另外一種是RDB,AOF是指在每次寫(xiě)操作后,將該操作序列化后追加到文件中,重啟時(shí)重放文件中的對(duì)應(yīng)操作,從而達(dá)到持久化的目的。其實(shí)現(xiàn)簡(jiǎn)單,用在MossDB是一個(gè)不錯(cuò)的選擇,但需要注意的是AOF缺點(diǎn)同樣明顯,如果文件較大,每次重啟會(huì)花費(fèi)較多時(shí)間。
Redis的AOF是一種后寫(xiě)式日志,先寫(xiě)內(nèi)存直接返回給用戶(hù),再寫(xiě)磁盤(pán)文件持久化,可以保證其高性能,但如果中途宕機(jī)會(huì)丟失數(shù)據(jù)。MossDB中的AOF采用了WAL(預(yù)寫(xiě)式日志)實(shí)現(xiàn),先寫(xiě)Log再寫(xiě)內(nèi)存,用來(lái)保證數(shù)據(jù)不會(huì)丟失,從而可以進(jìn)一步實(shí)現(xiàn)事務(wù)。
那么采用WAL會(huì)不會(huì)影響其性能?每次必須等到落盤(pán)后才進(jìn)行其他操作,WAL的每次寫(xiě)入會(huì)先寫(xiě)到內(nèi)核緩沖區(qū),這個(gè)調(diào)用很快就返回了,內(nèi)核再將數(shù)據(jù)落盤(pán)。我們也可以使用fsync調(diào)用強(qiáng)制內(nèi)核執(zhí)行直接將數(shù)據(jù)寫(xiě)入磁盤(pán)。在MossDB中普通寫(xiě)操作之會(huì)不會(huì)直接調(diào)用fsync,事務(wù)寫(xiě)中強(qiáng)制開(kāi)啟fsync,從而平衡數(shù)據(jù)一致性與性能。
WAL的實(shí)現(xiàn)引用了tiwall/wal,其中封裝了對(duì)Log文件的操作,可以支持批量寫(xiě)入。由于WAL是二進(jìn)制的,必須將數(shù)據(jù)進(jìn)行編碼,通過(guò)varint編碼實(shí)現(xiàn),將數(shù)據(jù)長(zhǎng)度插入到數(shù)據(jù)本體之前,讀取時(shí)可以讀取定長(zhǎng)的數(shù)據(jù)長(zhǎng)度,然后按長(zhǎng)度讀取數(shù)據(jù)本體。MossDB中數(shù)據(jù)格式如下:
type Record struct {
Op uint16
KeySize uint32
ValSize uint32
Timestamp uint64
TTL uint64
Key []byte
Val []byte
}對(duì)應(yīng)編碼后的二進(jìn)制格式為:
|------------------------------------------------------------|
| Op | KeySize | ValSize | Timestamp | TTL | Key | Val |
|------------------------------------------------------------|
| 2 | 4 | 4 | 8 | 8 | []byte | []byte |
|------------------------------------------------------------|
使用binary.BigEndian.PutUint16進(jìn)行編碼,解碼時(shí)通過(guò)binary.BigEndian.Uint16,從而依次取得生成完整的數(shù)據(jù)。
存儲(chǔ)引擎
MossDB提供了存儲(chǔ)接口,只要實(shí)現(xiàn)了此接口都可以作為其后端存儲(chǔ)
type Store interface {
Get(key string) ([]byte, bool)
Set(key string, val []byte)
Delete(key string)
Prefix(key string) map[string][]byte
Dump() map[string][]byte
Len() int
}內(nèi)置提供了HashMap與RadixTree兩種方式,HashMap實(shí)現(xiàn)簡(jiǎn)單通過(guò)簡(jiǎn)單封裝map可以快速進(jìn)行查詢(xún)與插入,但范圍搜索性能差。RadixTree即前綴樹(shù),查詢(xún)插入的時(shí)間復(fù)雜度只與Key的長(zhǎng)度相關(guān),而且支持范圍搜索,MossDB采用Adaptive Radix Tree可以避免原生的前準(zhǔn)樹(shù)空間浪費(fèi)。
由于RadixTree的特性,MossDB可以方便的進(jìn)行前綴搜索,目前支持List與Watch操作。
事務(wù)實(shí)現(xiàn)
要實(shí)現(xiàn)事務(wù)必須要保證其ACID特性,MossDB的事務(wù)定義如下:
type Tx struct {
db *DB // DB實(shí)例
commits []*Record // 對(duì)于寫(xiě)操作生成 Record, 用來(lái)做持久化
undos []*Record // 對(duì)于寫(xiě)操作生成 Undo Record,用于回滾
}MossDB中一次事務(wù)的流程主要包含以下幾個(gè)步驟:
- 首先加鎖,保證其數(shù)據(jù)一致性
- 對(duì)于寫(xiě)操作,生成Commits和Undo Records,然后寫(xiě)入內(nèi)存;讀操作則直接執(zhí)行
- 提交階段,將Commits持久化到WAL中;若寫(xiě)入失敗,則刪除已寫(xiě)入數(shù)據(jù);成功則設(shè)置數(shù)據(jù)的其他屬性(TTL, Watch等)
- 若中間發(fā)生錯(cuò)誤,執(zhí)行回滾操作,將Undo Records的記錄執(zhí)行
- 事務(wù)完成,釋放鎖
Watch
由于工作中經(jīng)常使用Kubernetes,對(duì)于其Watch接口印象深刻,通過(guò)Watch來(lái)充當(dāng)其事件總線(xiàn),保證其聲明式對(duì)象的管理。Kubernetes的Watch底層由etcd實(shí)現(xiàn),MossDB也實(shí)現(xiàn)了類(lèi)似的功能。
Watch的定義如下:
type Watcher struct {
mu sync.RWMutex // 鎖
watchers map[string]*SubWatcher // watchId與具體Watcher直接的映射
keys map[string]containerx.Set[string] // Watch單個(gè)key
ranges *art.Tree // 前綴Watch
queue workqueue.WorkQueue // 工作隊(duì)列,存放所有事件
stop chan struct{} // 是否中止
}通過(guò)工作隊(duì)列模式,任何寫(xiě)操作都會(huì)同步追加到隊(duì)列中,如果存在單個(gè)key的監(jiān)聽(tīng)者,則通過(guò)watchers map獲取到對(duì)應(yīng)列表,依次發(fā)送事件。對(duì)于前綴Watch,我們不可能記錄此前綴的所有Key,這里借鑒了etcd,通過(guò)RadixTree保存前綴Key,當(dāng)有新事件時(shí),匹配Key所在的路徑,如果有監(jiān)聽(tīng)者,則進(jìn)行事件通知。
調(diào)用Watch會(huì)返回一個(gè)Channel,用戶(hù)只需要監(jiān)聽(tīng)Channel即可
func Watch(db *mossdb.DB) {
key := "watch-key"
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Millisecond)
defer cancel()
// start watch key
ch := db.Watch(ctx, key)
log.Printf("start watch key %s", key)
go func() { // 模擬發(fā)送event
db.Set(key, mossdb.Val("val1"))
db.Set(key, mossdb.Val("val2"))
db.Delete(key)
time.Sleep(100 * time.Second)
db.Set(key, mossdb.Val("val3"))
}()
for {
select {
case <-ctx.Done():
log.Printf("context done")
return
case resp, ok := <-ch:
if !ok {
log.Printf("watch done")
return
}
log.Printf("receive event: %s, key: %s, new val: %s", resp.Event.Op, resp.Event.Key, resp.Event.Val)
}
}
// Output
// 2023/02/23 09:48:50 start watch key watch-key
// 2023/02/23 09:34:19 receive event: MODIFY, key: watch-key, new val: val1
// 2023/02/23 09:34:19 receive event: MODIFY, key: watch-key, new val: val2
// 2023/02/23 09:34:19 receive event: DELETE, key: watch-key, new val:
// 2023/02/23 09:34:19 context done
}TTL
過(guò)期刪除再很多場(chǎng)景很有用,比如驗(yàn)證碼過(guò)期、訂單未支付關(guān)閉等。MossDB采用時(shí)間堆來(lái)實(shí)現(xiàn)精確的Key過(guò)期策略,具體原理可以參考之前的文章Golang分布式應(yīng)用之定時(shí)任務(wù),在查詢(xún)操作時(shí)也會(huì)檢查Key是否過(guò)期,如果過(guò)期則直接返回空數(shù)據(jù)。配合Watch操作可以精確管理數(shù)據(jù)的生命周期。
總結(jié)
至此,MossDB的實(shí)現(xiàn)細(xì)節(jié)已經(jīng)分析完成,支持了事務(wù)、持久化、Watch與過(guò)期刪除等特性,后續(xù)可能會(huì)支持HTTP API、存儲(chǔ)快照等功能。
所有代碼見(jiàn)https://github.com/qingwave/mossdb,歡迎批評(píng)指正以及Star。
以上就是Golang實(shí)現(xiàn)事務(wù)型內(nèi)存數(shù)據(jù)庫(kù)的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang事務(wù)型內(nèi)存數(shù)據(jù)庫(kù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang的os標(biāo)準(zhǔn)庫(kù)中常用函數(shù)的整理介紹
這篇文章主要介紹了Go語(yǔ)言的os標(biāo)準(zhǔn)庫(kù)中常用函數(shù),主要用來(lái)實(shí)現(xiàn)與操作系統(tǒng)的交互功能,需要的朋友可以參考下2015-10-10
golang 函數(shù)返回chan類(lèi)型的操作
這篇文章主要介紹了golang 函數(shù)返回chan類(lèi)型的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
go語(yǔ)言中的json與map相互轉(zhuǎn)換實(shí)現(xiàn)
本文主要介紹了go語(yǔ)言中的json與map相互轉(zhuǎn)換實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08

