這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
上周我在Gopher China的分享內容中有講到我們遊戲伺服器上用了一套自己開發的記憶體資料庫,它從MySQL映射資料庫結構,並且支援事務。可能因為當時講的比較匆忙,加上PPT的篇幅有限,有些朋友對這部分還是有疑問,回來以後比較多朋友問我這個記憶體資料庫事務怎麼實現的,所以我趁今天有空門寫這一篇文章來細講一下這個無用技能。
在直接用資料庫的時候,我們都體驗過事務(沒體驗過請自行實驗),當一個業務需要多步驟的資料非查詢操作時,資料庫事務機制可以幫我們保證資料的完整性,不至於出現像銀行轉賬操作,錢款已扣除但是對方沒收到這樣的情況。
而一旦我們脫離了資料庫自己來管理資料,資料的完整性就需要自己維護了,最土的辦法就是按需來,每種業務都寫上各自的錯誤處理和資料復原邏輯,但是這樣是最不保險的,人是最容易犯錯的,所以需要想辦法把做一套事務機制。
我當初剛在設計《神仙道》的記憶體資料庫的時候在事務這塊卡了好久,這就是科班出身和野路子的最大差別,缺乏理論知識又沒做過,所以就不知道怎麼做了。但是想清楚了以後,發現原理非常簡單。
我們把非查詢操作歸類一下,無非就是插入、刪除、修改,這三類,我們再針對每一類研究復原方案,插入的資料在復原時需要刪除,刪除的資料在復原時需要插入,修改的資料需要在復原時修改回舊資料,就這三種情況。
要知道操作類型和操作的資料,就需要有一個列表來記錄事務中的操作,於是就有了交易記錄這樣一個東西。
交易記錄中的每一步都有對應的提交和復原動作,於是就有了這樣一個介面:
type TransLog interface { Commit(*Database) Rollback(*Database)}
假設我們的資料庫中有一張表叫player_item,記憶體中的資料庫結構映射大概像這樣:
type Database struct { transLogs []TransLog playerItem map[int]*PlayerItem}type PlayerItem struct { Id int ItemId int Num int}
在對這張表進行增刪改操作的時候,我們記錄下操作類型和新舊資料,於是我們就有了交易記錄。
func (db *Database) InsertPlayerItem(playerItem *PlayerItem) { db.playerItem[playerItem.Id] = playerItem db.transLogs = append(db.transLogs, &PlayerItemTransLog{ Type: INSERT, New: playerItem, })}func (db *Database) DeletePlayerItem(playerItem *PlayerItem) { old := db.playerItem[playerItem.Id] delete(db.playerItem, playerItem.Id) db.transLogs = append(db.transLogs, &PlayerItemTransLog{ Type: DELETE, Old: old, })}func (db *Database) UpdatePlayerItem(playerItem *PlayerItem) { old := db.playerItem[playerItem.Id] db.playerItem[playerItem.Id] = playerItem db.transLogs = append(db.transLogs, &PlayerItemTransLog{ Type: UPDATE, Old: old, New: playerItem, })}
因為資料庫只管用交易記錄介面,不管具體交易記錄的實現,所以我們需要實現player_item表的交易記錄:
type TransType intconst ( INSERT TransType = iota DELETE UPDATE)type PlayerItemTransLog struct { Type TransType Old *PlayerItem New *PlayerItem}func (transLog *PlayerItemTransLog) Commit(db *Database) { switch transLog.Type { case INSERT: fmt.Printf( "INSERT INTO player_item (id, item_id, num) VALUES (%d, %d, %d)\n", transLog.New.Id, transLog.New.ItemId, transLog.New.Num, ) case DELETE: fmt.Printf( "DELETE player_item WHERE id = %d\n", transLog.Old.Id, ) case UPDATE: fmt.Printf( "UPDATE player_item SET id = %d, item_id = %d, num = %d\n", transLog.New.Id, transLog.New.ItemId, transLog.New.Num, ) }}func (transLog *PlayerItemTransLog) Rollback(db *Database) { switch transLog.Type { case INSERT: delete(db.playerItem, transLog.New.Id) case DELETE: db.playerItem[transLog.Old.Id] = transLog.Old case UPDATE: db.playerItem[transLog.Old.Id] = transLog.Old }}
我們把復原和提交的邏輯封裝起來,於是記憶體資料庫就有了事務機制:
func (db *Database) Transaction(trans func()) { defer func() { if err := recover(); err != nil { for i := len(db.transLogs) - 1; i >= 0; i-- { db.transLogs[i].Rollback(db) } panic(err) } else { for _, tl := range db.transLogs { tl.Commit(db) } } db.transLogs = db.transLogs[0:0] }() trans()}
因為交易記錄是順序記錄的,後一步操作的資料可能由前一步產生,所以復原的時候需要倒序,從最後一步開始復原。
因為格式很固定,所以這些代碼很容易用代碼產生器產生。
完整的代碼在:https://github.com/idada/go-labs/blob/master/labs30/labs30.go
現在你就會發現所謂“記憶體資料庫”和“記憶體資料庫事務”完全就是標題黨嘛,沒什麼好神奇的,恭喜你又掌握一個無用技能 :)