一:需要選主的場景
1:服務有多臺機器,取其中一臺去執行任務。多臺機器同時執行會出問題,如將資料庫中狀態為失敗的記錄取出來重新執行,如果多臺機器同時執行,會導致一個失敗的任務被多臺機器同時執行。
2:服務有多臺機器,選其中一臺作為主,主負責任務的分發,大家一起消費並處理任務。還是將資料庫中狀態為失敗的記錄取出來重新執行,由於一臺機器可能處理不過來,需要多臺機器協同處理。這個時候主機器負責將失敗的記錄從資料庫中查出來,寫入訊息佇列,其他機器一同消費佇列中的任務,並處理失敗的記錄
二:進行選主
根據上面的選主場景,我們其實可以從多臺機器中隨機取一臺,比raft這種選主演算法簡單得多。我們甚至可以在配置檔案中指定一臺機器,只有這臺機器才執行相關功能,其他機器則不執行。如果是固定的幾臺機器,且一臺機器也能完成我們的需求,這樣搞其實也可以。如果機器不固定,而且單臺處理不過來時,用配置檔案的方式就不適合。
可採用競爭選主的方式,誰先搶到誰就是主。
1:方案一
採用redis方案實現。如果指定的key不存在就將機器資訊寫入這個key,成功寫入的那臺機器就是主,設定過期時間,防止機器異常掛掉的情況,所有的機器都需要定時去搶redis鎖。SETNX這個命令就滿足我們的需求,寫redis成功的就是主,寫失敗的就是從。
優點:
- 1:實現簡單,比配置檔案的方式好一點,支援機器動態
缺點:
- 1:需要定時去搶鎖
- 2:主可能經常變化,而且要保證主在切換的過程中業務邏輯的正確性
- 3:有些時間片可能沒有主,就是主掛掉了,而其他機器還沒到搶鎖的時間,這個時間片就沒有主
2:方案二
採用etcd方案實現。etcd支援事務能做到不存在就寫入,達到redis SETNX一樣的效果,而且通過etcd的租賃機制保證在主掛掉的情況下通知所有機器,這時大家自動開始新一輪的選主,還是那句話第一個搶到的就是主。
優點:
- 滿足我們的需求,沒有設計上的缺陷
- 只有主掛掉的情況,才會重新選主,不用擔心主在切換的過程中對業務邏輯的影響
缺點:
- 實現起來相對複雜,那我就來試試吧
golang原始碼實現如下:
1 package etcdDemo 2 3 import ( 4 "context" 5 "fmt" 6 "github.com/coreos/etcd/clientv3" 7 "github.com/google/uuid" 8 "time" 9 ) 10 11 type Callback func(isMaster bool) 12 13 type SelectMaster struct { 14 endPoints []string 15 key string 16 cli *clientv3.Client 17 lease *clientv3.LeaseGrantResponse 18 chClose chan int 19 callback Callback 20 token string 21 isMaster bool 22 } 23 24 func NewSelectMaster(endPoints []string, key string) (*SelectMaster, error) { 25 sm := &SelectMaster{ 26 endPoints: endPoints, 27 key: key, 28 chClose: make(chan int, 0), 29 token: uuid.New().String(), 30 } 31 32 cli, err := clientv3.New(clientv3.Config{ 33 Endpoints: endPoints, 34 DialTimeout: 3 * time.Second, 35 }) 36 if err != nil { 37 return sm, err 38 } 39 sm.cli = cli 40 go sm.ioLoop() 41 return sm, nil 42 } 43 44 func (sm *SelectMaster) ioLoop() { 45 fmt.Println("SelectMaster.ioLoop start") 46 ticker := time.NewTicker(time.Second * 3) 47 defer ticker.Stop() 48 chWatch := sm.cli.Watch(context.TODO(), sm.key) 49 for { 50 select { 51 case <-ticker.C: 52 if sm.lease == nil { 53 leaseResp, err := sm.cli.Grant(context.Background(), 4) 54 if err != nil { 55 fmt.Println("cli.Grant error=", err.Error()) 56 } else { 57 sm.lease = leaseResp 58 } 59 } 60 if sm.lease != nil { 61 _, err := sm.cli.KeepAliveOnce(context.Background(), sm.lease.ID) 62 if err != nil { 63 fmt.Println("cli.KeepAliveOnce error=", err.Error()) 64 break 65 } 66 } 67 case c := <-chWatch: 68 for _, e := range c.Events { 69 if e == nil || e.Kv == nil { 70 continue 71 } 72 token := string(e.Kv.Value) 73 sm.isMaster = sm.token == token 74 if sm.callback == nil { 75 fmt.Println("SelectMaster.callback is nil") 76 } else { 77 sm.callback(sm.isMaster) 78 fmt.Println("SelectMaster.isLoop token=", token) 79 if token == "" { //主掛了,開始競選 80 sm.election() 81 } 82 } 83 } 84 case <-sm.chClose: 85 goto stop 86 } 87 } 88 stop: 89 fmt.Println("SelectMaster.ioLoop end") 90 } 91 92 func (sm *SelectMaster) IsMaster() bool { 93 return sm.isMaster 94 } 95 96 func (sm *SelectMaster) Close() { 97 sm.chClose <- 1 98 } 99 100 func (sm *SelectMaster) Election(callback Callback) (bool, error) { 101 sm.callback = callback 102 return sm.election() 103 } 104 105 func (sm *SelectMaster) election() (bool, error) { 106 ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) 107 defer cancel() 108 leaseResp, err := sm.cli.Grant(ctx, 10) 109 if err != nil { 110 return false, err 111 } 112 sm.lease = leaseResp 113 txn := clientv3.NewKV(sm.cli).Txn(context.TODO()) 114 txn.If(clientv3.Compare(clientv3.CreateRevision(sm.key), "=", 0)). 115 Then(clientv3.OpPut(sm.key, sm.token, clientv3.WithLease(leaseResp.ID))).Else() 116 txnResp, err := txn.Commit() 117 if err != nil { 118 return false, err 119 } 120 return txnResp.Succeeded, nil 121 } 122 123 func testSelectMaster() *SelectMaster { 124 endPoints := []string{"172.25.20.248:2379"} 125 sm, err := NewSelectMaster(endPoints, "/test/lock") 126 if err != nil { 127 fmt.Println(err.Error()) 128 return nil 129 } 130 callback := func(isMaster bool) { 131 fmt.Println(sm.token, "callback=", isMaster) 132 } 133 isSuccess, err := sm.Election(callback) 134 if err != nil { 135 fmt.Println(sm.token, "Election=", err.Error()) 136 } else { 137 fmt.Println(sm.token, "Election=", isSuccess) 138 } 139 return sm 140 } 141 142 func TestSelectMaster() { 143 var master *SelectMaster 144 for i := 0; i < 3; i++ { 145 sm := testSelectMaster() 146 if sm.IsMaster() { 147 master = sm 148 } 149 } 150 if master != nil { 151 master.Close() 152 } 153 time.Sleep(time.Second*10) 154 }