完整mes代码(含客户端和server端_200行代码实现基于paxos的kv存储
本文鏈接: https://blog.openacid.com/algo/paxoskv/
前言
寫完 paxos的直觀解釋 之后, 網(wǎng)友都說療效甚好, 但是也會對這篇教程中一些環(huán)節(jié)提出疑問(有疑問說明真的看懂了 ) , 例如怎么把只能確定一個值的paxos應(yīng)用到實際場景中.
既然Talk is cheap, 那么就Show me the code, 這次我們把教程中描述的內(nèi)容直接用代碼實現(xiàn)出來, 希望能覆蓋到教程中的涉及的每個細(xì)節(jié). 幫助大家理解paxos的運行機制.
這是一個基于paxos, 200行代碼的kv存儲系統(tǒng)的簡單實現(xiàn), 作為 paxos的直觀解釋 這篇教程中的代碼示例部分. Paxos的原理本文不再介紹了, 本文提到的數(shù)據(jù)結(jié)構(gòu)使用protobuf定義, 網(wǎng)絡(luò)部分使用grpc定義. 另外200行g(shù)o代碼實現(xiàn)paxos存儲.
文中的代碼可能做了簡化, 完整代碼實現(xiàn)在 paxoskv 這個項目中(naive分支).
運行和使用
跑一下:
git clone https://github.com/openacid/paxoskv.git cd paxoskv go test -v ./...這個項目中除了paxos實現(xiàn), 用3個test case描述了3個paxos運行的例子,
- TestCase1SingleProposer: 無沖突運行.
- TestCase2DoubleProposer: 有沖突運行.
- Example_setAndGetByKeyVer: 作為key-val使用.
測試代碼描述了幾個paxos運行例子的行為, 運行測試可以確認(rèn)paxos的實現(xiàn)符合預(yù)期.
本文中 protobuf 的數(shù)據(jù)結(jié)構(gòu)定義如下:
service PaxosKV {rpc Prepare (Proposer) returns (Acceptor) {}rpc Accept (Proposer) returns (Acceptor) {} } message BallotNum {int64 N = 1;int64 ProposerId = 2; } message Value {int64 Vi64 = 1; } message PaxosInstanceId {string Key = 1;int64 Ver = 2; } message Acceptor {BallotNum LastBal = 1;Value Val = 2;BallotNum VBal = 3; } message Proposer {PaxosInstanceId Id = 1;BallotNum Bal = 2;Value Val = 3; }以及主要的函數(shù)實現(xiàn):
// struct KVServer Storage : map[string]Versions func Accept(c context.Context, r *Proposer) (*Acceptor, error) func Prepare(c context.Context, r *Proposer) (*Acceptor, error) func getLockedVersion(id *PaxosInstanceId) *Version// struct Proposer func Phase1(acceptorIds []int64, quorum int) (*Value, *BallotNum, error) func Phase2(acceptorIds []int64, quorum int) (*BallotNum, error) func RunPaxos(acceptorIds []int64, val *Value) (*Value) func rpcToAll(acceptorIds []int64, action string) ([]*Acceptor)func ServeAcceptors(acceptorIds []int64) ([]*grpc.Server)從頭實現(xiàn)paxoskv
Paxos 相關(guān)的數(shù)據(jù)結(jié)構(gòu)
在這個例子中我們的數(shù)據(jù)結(jié)構(gòu)和服務(wù)框架使用 protobuf 和 grpc 實現(xiàn), 首先是最底層的paxos數(shù)據(jù)結(jié)構(gòu):
Proposer 和 Acceptor
在 slide-27 中我們介紹了1個 Acceptor 所需的字段:
在存儲端(Acceptor)也有幾個概念:- last_rnd 是Acceptor記住的最后一次進(jìn)行寫前讀取的Proposer(客戶端)是誰, 以此來決定誰可以在后面真正把一個值寫到存儲中.
- v 是最后被寫入的值.
- vrnd 跟v是一對, 它記錄了在哪個Round中v被寫入了.
原文中這些名詞是參考了 paxos made simple 中的名稱, 但在 Leslie Lamport 后面的幾篇paper中都換了名稱, 為了后續(xù)方便, 在paxoskv的代碼實現(xiàn)中也做了相應(yīng)的替換:
rnd ==> Bal // 每一輪paxos的編號, BallotNum vrnd ==> VBal // 在哪個Ballot中v被Acceptor 接受(voted) last_rnd ==> LastBalProposer的字段也很簡單, 它需要記錄:
- 當(dāng)前的ballot number: Bal,
- 以及它選擇在Phase2運行的值: Val (slide-29).
于是在這個項目中用protobuf定義這兩個角色的數(shù)據(jù)結(jié)構(gòu), 如代碼 paxoskv.proto 中的聲明, 如下:
message Acceptor {BallotNum LastBal = 1;Value Val = 2;BallotNum VBal = 3; }message Proposer {PaxosInstanceId Id = 1;BallotNum Bal = 2;Value Val = 3; }其中Proposer還需要一個PaxosInstanceId, 來標(biāo)識當(dāng)前的paxos實例為哪個key的哪個version在做決定, paxos made simple 中只描述了一個paxos實例的算法(對應(yīng)一個key的一次修改), 要實現(xiàn)多次修改, 就需要增加這個字段來區(qū)分不同的paxos實例:
message PaxosInstanceId {string Key = 1;int64 Ver = 2; }paxoskv.proto 還定義了一個BallotNum, 因為要保證全系統(tǒng)內(nèi)的BallotNum都有序且不重復(fù), 一般的做法就是用一個本地單調(diào)遞增的整數(shù), 和一個全局唯一的id組合起來實現(xiàn):
message BallotNum {int64 N = 1;int64 ProposerId = 2; }定義RPC消息結(jié)構(gòu)
RPC消息定義了Proposer和Acceptor之間的通訊.
在一個paxos系統(tǒng)中, 至少要有4個消息:
- Phase1的 Prepare-request, Prepare-reply,
- 和Phase2的 Accept-request, Accept-reply,
如slide-28 所描述的(原文中使用rnd, 這里使用Bal, 都是同一個概念):
Phase-1(Prepare):``` request: Bal: int
reply: LastBal: int Val: string VBal: int ```
Phase-2(Accept):
``` request: Bal: int Val: string
reply: LastBal: int ```
在Prepare-request或Accept-request中, 發(fā)送的是一部分或全部的Proposer的字段, 因此我們在代碼中:
- 直接把Proposer的結(jié)構(gòu)體作為request的結(jié)構(gòu)體.
- 同樣把Acceptor的結(jié)構(gòu)體作為reply的結(jié)構(gòu)體.
在使用的時候只使用其中幾個字段. 對應(yīng)我們的 RPC 服務(wù) PaxosKV 定義如下:
service PaxosKV {rpc Prepare (Proposer) returns (Acceptor) {}rpc Accept (Proposer) returns (Acceptor) {} }使用protobuf和grpc生成服務(wù)框架
protobuf可以將paxoskv.proto直接生成go代碼( 代碼庫中已經(jīng)包含了生成好的代碼: paxoskv.pb.go, 只有修改paxoskv.proto 之后才需要重新生成)
- 首先安裝protobuf的編譯器 protoc, 可以根據(jù) install-protoc 中的步驟安裝, 一般簡單的一行命令就可以了:
- Linux: apt install -y protobuf-compiler
- Mac: brew install protobuf
安裝好之后通過protoc --version確認(rèn)版本, 至少應(yīng)該是3.x: libprotoc 3.13.0
- 安裝protoc的go語言生成插件 protoc-gen-go:
go get -u github.com/golang/protobuf/protoc-gen-go
- 重新編譯protokv.proto文件: 直接make gen 或:
protoc --proto_path=proto --go_out=plugins=grpc:paxoskv paxoskv.proto
生成后的paxoskv.pb.go代碼中可以看到, 其中主要的數(shù)據(jù)結(jié)構(gòu)例如Acceptor的定義:
type Acceptor struct {LastBal *BallotNum ...Val *Value ...VBal *BallotNum ...... }以及KV服務(wù)的client端和server端的代碼, client端是實現(xiàn)好的, server端只有一個interface, 后面我們需要來完成它的實現(xiàn):
type paxosKVClient struct {cc *grpc.ClientConn } type PaxosKVClient interface {Prepare(ctx context.Context,in *Proposer,opts ...grpc.CallOption) (*Acceptor, error)Accept(ctx context.Context,in *Proposer,opts ...grpc.CallOption) (*Acceptor, error) }type PaxosKVServer interface {Prepare(context.Context,*Proposer) (*Acceptor, error)Accept(context.Context,*Proposer) (*Acceptor, error) }實現(xiàn)存儲的服務(wù)器端
impl.go 是所有實現(xiàn)部分, 我們定義一個KVServer結(jié)構(gòu)體, 用來實現(xiàn)grpc服務(wù)的interface PaxosKVServer; 其中使用一個內(nèi)存里的map結(jié)構(gòu)模擬數(shù)據(jù)的存儲:
type Version struct {mu sync.Mutexacceptor Acceptor } type Versions map[int64]*Version type KVServer struct {mu sync.MutexStorage map[string]Versions }其中Version對應(yīng)一個key的一次變化, 也就是對應(yīng)一個paxos實例. Versions對應(yīng)一個key的一系列變化. Storage就是所有key的所有變化.
實現(xiàn) Acceptor 的 grpc 服務(wù) handler
Acceptor, 是這個系統(tǒng)里的server端, 監(jiān)聽一個端口, 等待Proposer發(fā)來的請求并處理, 然后給出應(yīng)答.
根據(jù)paxos的定義, Acceptor的邏輯很簡單: 在 slide-28 中描述:
根據(jù)教程里的描述, 為 KVServer 定義handle Prepare-request的代碼:
func (s *KVServer) Prepare(c context.Context,r *Proposer) (*Acceptor, error) {v := s.getLockedVersion(r.Id)defer v.mu.Unlock()reply := v.acceptorif r.Bal.GE(v.acceptor.LastBal) {v.acceptor.LastBal = r.Bal}return &reply, nil }這段代碼分3步:
- 取得paxos實例,
- 生成應(yīng)答: Acceptor總是返回LastBal, Val, VBal 這3個字段, 所以直接把Acceptor賦值給reply.
- 最后更新Acceptor的狀態(tài): 然后按照paxos算法描述, 如果請求中的ballot number更大, 則記錄下來, 表示不在接受更小ballot number的Proposer.
其中g(shù)etLockedVersion() 從KVServer.Storage中根據(jù)request 發(fā)來的PaxosInstanceId中的字段key和ver獲取一個指定Acceptor的實例:
func (s *KVServer) getLockedVersion(id *PaxosInstanceId) *Version {s.mu.Lock()defer s.mu.Unlock()key := id.Keyver := id.Verrec, found := s.Storage[key]if !found {rec = Versions{}s.Storage[key] = rec}v, found := rec[ver]if !found {// initialize an empty paxos instancerec[ver] = &Version{acceptor: Acceptor{LastBal: &BallotNum{},VBal: &BallotNum{},},}v = rec[ver]}v.mu.Lock()return v }handle Accept-request的處理類似, 在 slide-31 中描述:
Accept() 要記錄3個值,
- LastBal: Acceptor看到的最大的ballot number;
- Val: Proposer選擇的值,
- 以及VBal: Proposer的ballot number:
Acceptor 的邏輯到此完整了, 再看Proposer:
實現(xiàn)Proposer 邏輯
Proposer的運行分2個階段, Phase1 和 Phase2, 與 Prepare 和 Accept 對應(yīng).
Phase1
在 impl.go 的實現(xiàn)中, Proposer.Phase1()函數(shù)負(fù)責(zé)Phase1的邏輯:
func (p *Proposer) Phase1(acceptorIds []int64,quorum int) (*Value, *BallotNum, error) {replies := p.rpcToAll(acceptorIds, "Prepare")ok := 0higherBal := *p.BalmaxVoted := &Acceptor{VBal: &BallotNum{}}for _, r := range replies {if !p.Bal.GE(r.LastBal) {higherBal = *r.LastBalcontinue}if r.VBal.GE(maxVoted.VBal) {maxVoted = r}ok += 1if ok == quorum {return maxVoted.Val, nil, nil}}return nil, &higherBal, NotEnoughQuorum }這段代碼首先通過 rpcToAll() 向所有Acceptor發(fā)送Prepare-request請求, 然后找出所有的成功的reply:
- 如果發(fā)現(xiàn)一個更大的ballot number, 表示一個Prepare失敗: 有更新的Proposer存在;
- 否則, 它是一個成功的應(yīng)答, 再看它有沒有返回一個已經(jīng)被Acceptor接受(voted)的值.
最后, 成功應(yīng)答如果達(dá)到多數(shù)派(quorum), 則認(rèn)為Phase1 完成, 返回最后一個被voted的值, 也就是VBal最大的那個. 讓上層調(diào)用者繼續(xù)Phase2;
如果沒有達(dá)到quorum, 這時可能是有多個Proposer并發(fā)運行而造成沖突, 有更大的ballot number, 這時則把見到的最大ballot number返回, 由上層調(diào)用者提升ballot number再重試.
client 與 server 端的連接
上面用到的 rpcToAll 在這個項目中的實現(xiàn)client端(Proposer)到server端(Acceptor)的通訊, 它是一個十分 ~~簡潔美觀~~ 簡陋的 grpc 客戶端實現(xiàn):
func (p *Proposer) rpcToAll(acceptorIds []int64,action string) []*Acceptor {replies := []*Acceptor{}for _, aid := range acceptorIds {var err erroraddress := fmt.Sprintf("127.0.0.1:%d",AcceptorBasePort+int64(aid))conn, err := grpc.Dial(address, grpc.WithInsecure())if err != nil {log.Fatalf("did not connect: %v", err)}defer conn.Close()c := NewPaxosKVClient(conn)ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()var reply *Acceptorif action == "Prepare" {reply, err = c.Prepare(ctx, p)} else if action == "Accept" {reply, err = c.Accept(ctx, p)}if err != nil {continue}replies = append(replies, reply)}return replies }Phase2
Proposer運行的Phase2 在slide-30 中描述, 比Phase1更簡單:
在第2階段phase-2, Proposer X將它選定的值寫入到Acceptor中, 這個值可能是它自己要寫入的值, 或者是它從某個Acceptor上讀到的v(修復(fù)).func (p *Proposer) Phase2(acceptorIds []int64,quorum int) (*BallotNum, error) {replies := p.rpcToAll(acceptorIds, "Accept")ok := 0higherBal := *p.Balfor _, r := range replies {if !p.Bal.GE(r.LastBal) {higherBal = *r.LastBalcontinue}ok += 1if ok == quorum {return nil, nil}}return &higherBal, NotEnoughQuorum }我們看到, 它只需要確認(rèn)成 Phase2 的功應(yīng)答數(shù)量達(dá)到quorum就可以了. 另外同樣它也有責(zé)任在 Phase2 失敗時返回看到的更大的ballot number, 因為在 Phase1 和 Phase2 之間可能有其他 Proposer 使用更大的ballot number打斷了當(dāng)前Proposer的執(zhí)行, 就像slide-33 的沖突解決的例子中描述的那樣. 后面講.
完整的paxos邏輯
完整的 paxos 由 Proposer 負(fù)責(zé), 包括: 如何選擇一個值, 使得一致性得以保證. 如 slide-29 中描述的:
Proposer X收到多數(shù)(quorum)個應(yīng)答, 就認(rèn)為是可以繼續(xù)運行的.如果沒有聯(lián)系到多于半數(shù)的acceptor, 整個系統(tǒng)就hang住了, 這也是paxos聲稱的只能運行少于半數(shù)的節(jié)點失效. 這時Proposer面臨2種情況:所有應(yīng)答中都沒有任何非空的v, 這表示系統(tǒng)之前是干凈的, 沒有任何值已經(jīng)被其他paxos客戶端完成了寫入(因為一個多數(shù)派讀一定會看到一個多數(shù)派寫的結(jié)果). 這時Proposer X繼續(xù)將它要寫的值在phase-2中真正寫入到多于半數(shù)的Acceptor中.
如果收到了某個應(yīng)答包含被寫入的v和vrnd, 這時, Proposer X 必須假設(shè)有其他客戶端(Proposer) 正在運行, 雖然X不知道對方是否已經(jīng)成功結(jié)束, 但任何已經(jīng)寫入的值都不能被修改!, 所以X必須保持原有的值. 于是X將看到的最大vrnd對應(yīng)的v作為X的phase-2將要寫入的值.
這時實際上可以認(rèn)為X執(zhí)行了一次(不知是否已經(jīng)中斷的)其他客戶端(Proposer)的修復(fù).
基于 Acceptor 的服務(wù)端和 Proposer 2個 Phase 的實現(xiàn), 最后把這些環(huán)節(jié)組合到一起組成一個完整的paxos, 在我們的代碼 RunPaxos 這個函數(shù)中完成這些事情:
func (p *Proposer) RunPaxos(acceptorIds []int64,val *Value) *Value {quorum := len(acceptorIds)/2 + 1for {p.Val = valmaxVotedVal, higherBal, err := p.Phase1(acceptorIds, quorum)if err != nil {p.Bal.N = higherBal.N + 1continue}if maxVotedVal != nil {p.Val = maxVotedVal}// val == nil 是一個讀操作,// 沒有讀到voted值不需要Phase2if p.Val == nil {return nil}higherBal, err = p.Phase2(acceptorIds, quorum)if err != nil {p.Bal.N = higherBal.N + 1continue}return p.Val} }這段代碼完成了幾件事: 運行 Phase1, 有voted的值就選它, 沒有就選自己要寫的值val, 然后運行 Phase2.
就像 Phase1 Phase2 中描述的一樣, 任何一個階段, 如果沒達(dá)到quorum, 就需要提升遇到的更大的ballot number, 重試去解決遇到的ballot number沖突.
這個函數(shù)接受2個參數(shù):
- 所有Acceptor的列表(用一個整數(shù)的id表示一個Acceptor),
- 以及要提交的值.
其中, 按照paxos的描述, 這個值val不一定能提交: 如果paxos在 Phase1 完成后看到了其他已經(jīng)接受的值(voted value), 那就要選擇已接收的值, 放棄val. 遇到這種情況, 在我們的系統(tǒng)中, 例如要寫入key=foo, ver=3的值為bar, 如果沒能選擇bar, 就要選擇下一個版本key=foo, ver=4再嘗試寫入.
這樣不斷的重試循環(huán), 寫操作最終都能成功寫入一個值(一個key的一個版本的值).
實現(xiàn)讀操作
在我們這個NB(naive and bsice)的系統(tǒng)中, 讀和寫一樣都要通過一次paxos算法來完成. 因為寫入過程就是一次paxos執(zhí)行, 而paxos只保證在一個quorum中寫入確定的值, 不保證所有節(jié)點都有這個值. 因此一次讀操作如果要讀到最后寫入的值, 至少要進(jìn)行一次多數(shù)派讀.
但多數(shù)派讀還不夠: 它可能讀到一個未完成的paxos寫入, 如 slide-11 中描述的臟讀問題, 讀取到的最大VBal的值, 可能不是確定的值(寫入到多數(shù)派).
例如下面的狀態(tài):
Val=foo Val=bar ? VBal=3 VBal=2 ? ------- ------- -- A0 A1 A2如果Proposer試圖讀, 在 Phase1 聯(lián)系到A0 A1這2個Acceptor, 那么foo和bar這2個值哪個是確定下來的, 要取決于A2的狀態(tài). 所以這時要再把最大VBal的值跑完一次 Phase2, 讓它被確定下來, 然后才能把結(jié)果返回給上層(否則另一個Proposer可能聯(lián)系到A1 和 A2, 然后認(rèn)為Val=bar是被確定的值).
當(dāng)然如果 Proposer 在讀取流程的 Phase1 成功后沒有看到任何已經(jīng)voted的值(例如沒有看到foo或bar), 就不用跑 Phase2 了.
所以在這個版本的實現(xiàn)中, 讀操作也是一次 RunPaxos 函數(shù)的調(diào)用, 除了它并不propose任何新的值, 為了支持讀操作, 所以在上面的代碼中 Phase2 之前加入一個判斷, 如果傳入的val和已voted的值都為空, 則直接返回:
if p.Val == nil {return nil }Example_setAndGetByKeyVer 這個測試用例展示了如何使用paxos實現(xiàn)一個kv存儲, 實現(xiàn)讀和寫的代碼大概這樣:
prop := Proposer{Id: &PaxosInstanceId{Key: "foo",Ver: 0,},Bal: &BallotNum{N: 0, ProposerId: 2}, }// 寫: v := prop.RunPaxos(acceptorIds, &Value{Vi64: 5})// 讀: v := prop.RunPaxos(acceptorIds, nil)到現(xiàn)在為止, 本文中涉及到的功能都實現(xiàn)完了, 完整實現(xiàn)在 impl.go 中.
接著我們用測試用例實現(xiàn)1下 paxos的直觀解釋 中列出的2個例子, 從代碼看poxos的運行:
文中例子
第1個例子是 paxos 無沖突的運行 slide-32:
把它寫成test case, 確認(rèn)教程中每步操作之后的結(jié)果都如預(yù)期 TestCase1SingleProposer:
func TestCase1SingleProposer(t *testing.T) {ta := require.New(t)acceptorIds := []int64{0, 1, 2}quorum := 2// 啟動3個Acceptor的服務(wù)servers := ServeAcceptors(acceptorIds)defer func() {for _, s := range servers {s.Stop()}}()// 用要更新的key和version定義paxos 實例的idpaxosId := &PaxosInstanceId{Key: "i",Ver: 0,}var val int64 = 10// 定義Proposer, 隨便選個Proposer id 10.var pidx int64 = 10px := Proposer{Id: paxosId,Bal: &BallotNum{N: 0, ProposerId: pidx},}// 用左邊2個Acceptor運行Phase1,// 成功, 沒有看到其他的ballot numberlatestVal, higherBal, err := px.Phase1([]int64{0, 1}, quorum)ta.Nil(err, "constitued a quorum")ta.Nil(higherBal, "no other proposer is seen")ta.Nil(latestVal, "no voted value")// Phase1成功后, 因為沒有看到其他voted的值,// Proposer選擇它自己的值進(jìn)行后面的Phase2px.Val = &Value{Vi64: val}// Phase 2higherBal, err = px.Phase2([]int64{0, 1}, quorum)ta.Nil(err, "constitued a quorum")ta.Nil(higherBal, "no other proposer is seen") }第2個例子對應(yīng)2個Proposer遇到?jīng)_突并解決沖突的例子, 略長不貼在文中了, 代碼可以在 TestCase2DoubleProposer 看到
下一步
我們實現(xiàn)了指定key, ver的存儲系統(tǒng), 但相比真正生產(chǎn)可用的kv存儲, 還缺少一些東西:
- 寫操作一般都不需要用戶指定ver, 所以還需要實現(xiàn)對指定key查找最大ver的功能. 這些跟paxos關(guān)系不大, 現(xiàn)在這個實現(xiàn)中就省去了這些邏輯. 以后再講.
- 其次為了讓讀操作不需要指定ver, 還需要一個snapshot功能, 也就是保存一個key-value的map, 這個map中只需要記錄每個key最新的value值(以及ver等). 有了這個map之后, 已經(jīng)確認(rèn)的值對應(yīng)的version就可以刪掉了. 也就是說Versions 結(jié)構(gòu)只作為每個key的修改日志存在, 用于存儲每次修改對應(yīng)的paxos實例.
- snapshot功能還會引入應(yīng)另外一個需求, 就是paxos made simple 中的 learn 的行為, 對應(yīng)Phase3, 本文中描述的這個存儲中, 只有Proposer知道某個key-ver達(dá)到多數(shù)派, Acceptor還不知道, (所以讀的時候還要走一遍paxos). 在論文中的描述是Acceptor接受一個值時(vote), 也要把這個事情通知其他 Learner角色, 我們可以給每個Acceptor也設(shè)定成Learner: Acceptor vote一個值時除了應(yīng)答Proposer, 也廣播這個事件給其他Acceptor, 這樣每個Acceptor也就可以知道哪個值是達(dá)到quorum了(safe), 可以直接被讀取.
但在實際實現(xiàn)時, 這種方法產(chǎn)生的消息會達(dá)到 n2 級別的數(shù)量. 所以一般做法是讓Proposer做這件事: 當(dāng)Proposer收到一個quorum的Phase2應(yīng)答后, 再廣播一條消息告訴所有的Acceptor: 這個paxos實例已經(jīng)safe了, 這個消息在大多數(shù)系統(tǒng)中都就稱作Commit.
以上這3塊內(nèi)容, 后續(xù)播出, 下個版本的實現(xiàn)將使用經(jīng)典的log 加 snapshot的方式存儲數(shù)據(jù).
各位朋友對哪些方面感興趣, 歡迎催更 …
本文用到的代碼在 paxoskv 項目的 naive 分支上: https://github.com/openacid/paxoskv/tree/naive
如有什么本文遺漏的地方, 或有任何好想法, 歡迎隨時交流討論,
本文相關(guān)問題可以在 paxoskv 這個項目上提 基hub issue.
本文鏈接: https://blog.openacid.com/algo/paxoskv/
總結(jié)
以上是生活随笔為你收集整理的完整mes代码(含客户端和server端_200行代码实现基于paxos的kv存储的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 蓝牙耳机测试用例_移动端测试用例设计总结
- 下一篇: python动态柱状图_python –