Functional Go: Transient 及持久化

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

在之前的文章中,我們介紹了一些持久化資料結構實現的基本原理和 Vector Trie 這一資料結構在 Golang 下的實現過程。這篇文章終於來到了實現持久化 List 的最後一步: 實現 Transient 和持久化的功能。

這篇文章是系列文章的一部分,如果還沒有瀏覽過文章的其它部分請參考:

  1. 持久化資料結構簡介
  2. Vector Trie 的實現
  3. Transient 及持久化 (本文)

在之前的文章中,我們已經看到了如何?一個 Vector Trie,也知道如何使用 Vector Trie 來實現共用資料結構的持久化 List:在每次修改時,我們複製從根節點到被修改節點路徑上的所有節點,並使用得到的新的 Root 節點構造一個新的 List 的 HEAD資料結構。這樣通過新的 HEAD 我們就可以訪問到新的資料,通過舊的 HEAD 我們可以得到舊的資料。

按照這樣的思路,我們需要更改 List 的介面,對於每一個會修改 List 元素的操作,我們都要返回一個新的 List對象而不是在原來的對象上修改。比如說:

1
2
3
4
5
6
7
type List interface {
Get(n int) (interface{}, bool)
Set(n int, value interface{}) (List, bool)
PushBack(value interface{}) List
RemoveBack() (List, interface{})
Len() int
}

為了實現這樣的介面,我們有兩種選擇:

  1. 對於 SetPushBackRemoveBack 函數,我們把它們修改成返回新的 listHead 的形式
  2. 先實現 TransientList,對於SetPushBackRemoveBack 函數,我們把它改造成先把自己轉換成 TransientList並修改,最後返回將 Transient 持久化的結果

由於我們之前的代碼設計上預先做了準備,兩種方案的實現難度其實差別不大。但是因為 Transient支援對資料結構一連串操作的高效執行,我們決定採用第二種方案。第二種方案也會使得代碼更簡潔、複用的程度更高。

那麼什麼是 Transient 呢?下面我們就來介紹它。

Transient 的原理

前面說道,持久化資料結構的實現原理是複製一條路徑上的節點。在我們的設計中,每個節點的寬度是32,那麼如果我們連續地修改幾個相鄰的元素,即使這些元素都在一個葉子節點上,它也會被複製很多遍。這樣的行為是非常低效的。為了能讓我們高效地進行一連串的修改,一種解決方案就是允許一個持久化資料結構臨時地變成非持久的,在我們一連串的修改之後,再轉變回來。這樣每次修改都會在原地進行,從而極大地改善了效能。這裡臨時產生的非持久化資料結構就是我們所說的 Transient。

但是同樣我們也要知道,使用 Transient 是有一定風險的。首先作為一種可變資料結構,它一般來說會被實現為非安全執行緒的類型,因此如果並發地操作它,就可能產生 Race condition 等問題。另外,如果使用者在使用時保留了對於 Transient 的引用,把 Transient 轉變為持久化之後仍然對 Transient 進行了修改,那麼產生的持久化對象實際上也會被改變。也就是說,引入 Transient 可能會導致無效的持久化。

儘管 Transient 帶來了一些風險,但是考慮到效能上的提升,它還是值得的。Transient 的實現有兩個關鍵點:

  1. 為每個 Transient 分配一個全域唯一的id,當 Transient 每次對內部結構進行修改時,保證修改過的節點都被打上這個id作為標記
  2. 當 Transient 每次需要對節點進行修改時,它先檢查目標節點是否和自己有相同的id,如果相同,那麼這個節點是自己之前曾經修改或複製過的,因此可以在節點上直接進行修改。否則這個節點可能是之前的 Transient 產生的,為了防止改變原來的資料,我們應該複製當前節點一份。

這兩條策略保證我們可以安全地修改 Transient 而不會改變原來的資料。關鍵在於,通過為 Vector Trie 的節點打上id標誌,Transient 可以判斷一個節點的記憶體是不是由自己分配出來的。對於id不一樣的節點,它是由當前 List 修改曆史上出現過的某個 Transient 產生的,而之前那個 Transient 可能已經被轉換為持久對象,因此那些節點不應該被直接修改。而如果id一致,則表明當前 Transient 新近修改過這一節點,我們就可以再次修改。這一步是基於持久化過的 Transient不再會被使用者修改的假定。這也是為什麼如果 Transient 持久化之後仍被修改,則產生的持久化對象的不可變性就會被破壞的原因。

對比了持久化 List 和 Transient 在執行修改時的不同。

圖中上方是不使用 Transient 時的情況,右邊三種不同的顏色代表連續的三次修改。在這種情況下,我們的每次修改都會產生一個新的 Root 節點和一份新的葉子節點。這樣顯然是沒有效率的。

在圖中的下方是使用了 Transient 時的情況,每個節點都包含了一個 ID (圖中紫色標記),當第一次修改進行的時候我們為 Transient分配了一個新的 ID b,在修改的過程中檢查需要更新的節點,發現他們都具有 ID a,與當前 ID 不同,因此需要進行一次複製。在接下來的兩次修改中,由於 Transient 在其生命週期中 ID 不變,進行修改時發現目標節點的 ID 與當前 Transient 一致,因此我們不需要再複製節點,可以直接進行 In-place 的更新。

以上就是 Transient 的基本原理。由這個基本原理可以看出,實際上 Transient 和我們的持久化 List可以共用一套底層的資料結構,其差別僅在於 Transient 擁有一個 ID 而 List 沒有。實際上,為了區分這兩種情況,我們為所有的 List 的 HEAD 分配一個特殊的 ID,譬如0。在 List 和 Transient 之間轉換可以使用下列手段:

  1. 將 List 轉化為 Transient,我們使用某種 ID 產生器產生一個唯一且不同於 List ID 的 ID (如正整數)並分配給 List
  2. 將 Transient 轉為 List,我們將 Transient 的 ID 重設為 List ID (如0)

在我們的情況下, 由於 Golang 特殊的物件導向設計,我們實際上可以將 List 內部資料結構實現為 Transient 內部資料結構的一個 alias。

Transient 的實現

Unique ID 產生器的實現

對於 Transient 來講,如何為每次修改產生獨一無二的 ID 是一件重要的問題。在現實中存在很多功能各異的獨特 ID 產生演算法,他們有的只能工作在單機情況下,有的可以保證分布式情況下的唯一性,有的產生成本比較高,有的則非常輕量。在這裡,我們選擇最簡單的一種方式:累計uint64類型的正整數。

通過在單例情況下累計正整數的方式,我們可以保證產生的 Unique ID 在當前進程中是具有唯一性的。其原理是通過sync/atomic包下的原子操作AddUint64來實現遞增操作。這一操作既快速又安全執行緒。

以下是實現這一功能的內部包counter:

1
2
3
4
5
6
7
8
9
package counter

import "sync/atomic"

var id uint64 = 0

func Next() uint64 {
return atomic.AddUint64(&id, 1)
}

List 介面的更新

前面我們說道,可以將 List 實現為 Transient 的一個 alias。在這一步,我們先將之前部落格裡實現的 List 內部資料結構重新命名為tListHead,代表他是一個 Transient List 的 Head,之前實現的方法也都一併轉移過來。除此之外,我們還要在新的tListHead和它內部的 Trie 樹節點上都添加 ID 欄位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Transient List Head
type tListHead struct {
id uint64
len int
level int
offset int
root *trieNode
tail *trieNode
}

// Trie Node
type trieNode struct {
id uint64
children []interface{}
}

然後我們重新定義 List 的介面和實現方法:

1
2
3
4
5
6
7
8
9
10
type List interface {
Get(n int) (interface{}, bool)
Set(n int, value interface{}) (List, bool)
PushBack(value interface{}) List
RemoveBack() (List, interface{})
TransientList() TransientList
Len() int
}

type listHead tListHead

新的介面不再是在原來的基礎上進行修改,而是每次操作都返回新的 List 對象。我們還添加了一個方法用於將當前 List轉換為一個 Transient。注意到listHead只是tListHead的一個 alias,因此在 Go 語言中他們之間可以相互類型轉換。接下來我們定義一個全域公用的 empty 變數代表空的 List,由於我們希望所有的空 List 都一樣而持久化 List 是不會被改變的,因此我們並不會在 New 時建立新的Null 物件而是每次都返回這一個對象。這樣也節約了建立 List 時的記憶體消耗。

1
2
3
4
5
var empty = &listHead{0, 0, 0, 0, nil, nil}

func New() List {
return empty
}

List 的 Get 因為不會改變元素的值,我們直接通過類型轉化的方法將listHead轉換為tListHead並調用後者的對應方法獲得結果:

1
2
3
func (head *listHead) Get(n int) (interface{}, bool) {
return (*tListHead)(head).Get(n)
}

對於其它修改操作,我們都先將其轉換為 Transient 執行完修改操作之後再持久化回來。這樣就可以獲得新的 List 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (head *listHead) Set(n int, value interface{}) (List, bool) {
t := head.TransientList()
if t.Set(n, value) {
return t.Persist(), true
} else {
return head, false
}
}

func (head *listHead) PushBack(value interface{}) List {
t := head.TransientList()
t.PushBack(value)
return t.Persist()
}

func (head *listHead) RemoveBack() (List, interface{}) {
if head.len == 1 {
value, _ := head.Get(0)
return empty, value
} else {
t := head.TransientList()
value := t.RemoveBack()
return t.Persist(), value
}
}

下面給出了TransientList方法的實現。由於 List 的不可變性質需要被保留,因此轉化為 Transient 實際上需要建立立一個 tListHead,這樣對於 Transient 的修改就不會影響到原來的 List。這裡調用了之前實現的 counter包來獲得 Unique ID。

1
2
3
4
func (head *listHead) TransientList() TransientList {
id := counter.Next()
return &tListHead{id, head.len, head.level, head.offset, head.root, head.tail}
}

Transient 修改操作的實現

接下來還需要更新 Transient 修改操作的實現來保證不會影響到其它 Transient 以及之前的持久化 List。之前的 List 在實現的過程中我們已經部分考慮到這種問題了,大部分操作被設計為遞迴執行,同時對 Trie 樹的遞迴操作會賦值給原來節點。在這一基礎上我們首先為 trieNode 添加clonesetChild兩個方法。

clone 方法會將當前節點的內容複寫一邊並返回新的節點,它接受一個id參數,新複製出來的節點的id屬性會被設定為這一參數。

1
2
3
4
5
func (node *trieNode) clone(id uint64) *trieNode {
children := make([]interface{}, NODE_SIZE)
copy(children, node.children)
return &trieNode{id, children}
}

setChild 則是方便實現修改節點功能的函數,它的第一個參數也是id。如果傳入的id與節點原來的id相同,則這一方法直接在原來的節點上進行修改並返回原來的節點,否則它將會clone節點並在新的節點上進行操作。

1
2
3
4
5
6
7
8
9
10
func (node *trieNode) setChild(id uint64, n int, child interface{}) *trieNode {
if node.id == id {
node.children[n] = child
return node
} else {
newNode := node.clone(id)
newNode.children[n] = child
return newNode
}
}

之前 List 修改操作的各個內建函式也都被加上了id作為參數。除此之外,如果Set前後 List 包含值相同,我們希望實際效果是對象沒有被修改,在這一步我們也做了一些小心的操作來儘可能保證。具體的代碼不再贅述,完整的代碼請參考這個檔案。

下面是將 Transient 轉化為持久化的函數,由於我們預期使用者在將 Transient 持久化之後不會再修改原來的 Transient(儘管無法從代碼上保證),所以我們可以簡單地使用類型轉換來將 tListHead 轉換為 listHead

1
2
3
4
5
func (head *tListHead) Persist() List {
perisitHead := (*listHead)(head)
perisitHead.id = 0
return perisitHead
}

總結

這篇文章介紹了 Transient 的實現原理和最終實現持久化 List 的方法。可以看出 Transient 是為了提高持久化 List在連續修改操作下的效率而引進的資料結構,同時引入 Transient 也會簡化持久化 List 實現的複雜度。但是如果使用者以不正確的方式使用 Transient ,可能會破壞持久化 List 的持久性。在 Transient 存在的情況下,持久化 List 的修改操作被實現為先轉換為 Transient 並修改,最終將 Transient 持久化這樣的方法。

至此,我們就實現了一個功能較為完整的持久化 List 類。持久化 List 類是持久化資料結構當中最容易實現的一種,但是通過研究它的實現過程,我們可以體會到實現持久化資料結構的一些主要思路。這篇文章的結束宣告 Functional Go這一系列 Blog 暫時告一段落。下一個系列將會開始探討另一類非常重要的資料結構 Map 的持久化實現方法(Hash Array Mapped Trie)。

本文實現的代碼已經開源在 GitHub 上。按照計劃,配合 Blog 的更新,我也會繼續將進一步實現的持久化資料結構添加到這一倉庫中。也歡迎各位讀者對我實現的代碼提出意見建議或反饋 Bug 以及貢獻代碼。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.