在golang中實(shí)現(xiàn)分布式鎖需考慮安全性、可靠性與性能,主要方案包括:1. 基于redis的分布式鎖,使用setnx命令和過(guò)期時(shí)間實(shí)現(xiàn),優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單、性能高,缺點(diǎn)是可能存在鎖過(guò)期或續(xù)租機(jī)制復(fù)雜;2. 基于zookeeper的分布式鎖,利用臨時(shí)順序節(jié)點(diǎn)實(shí)現(xiàn),可靠性高但性能較低且實(shí)現(xiàn)較復(fù)雜;3. 基于etcd的分布式鎖,采用lease和watch機(jī)制,性能較高且易于部署,但實(shí)現(xiàn)也較復(fù)雜。選擇方案時(shí)應(yīng)根據(jù)業(yè)務(wù)需求權(quán)衡性能與可靠性,redis適合高性能場(chǎng)景,zookeeper和etcd適合高可靠性場(chǎng)景。續(xù)租機(jī)制用于延長(zhǎng)鎖的有效期,防止任務(wù)執(zhí)行期間鎖被釋放;避免誤刪除可通過(guò)設(shè)置唯一value并在釋放鎖時(shí)進(jìn)行校驗(yàn)實(shí)現(xiàn),確保僅刪除自身持有的鎖。
在golang中實(shí)現(xiàn)分布式鎖,需要考慮鎖的安全性、可靠性和性能。簡(jiǎn)單來(lái)說(shuō),就是確保在分布式環(huán)境下,只有一個(gè)客戶(hù)端能獲得鎖,并且即使發(fā)生故障,鎖也能被正確釋放。
解決方案
實(shí)現(xiàn)分布式鎖,通常有以下幾種方案,各有優(yōu)劣:
-
基于redis的分布式鎖: 利用redis的SETNX (SET if Not Exists) 命令和過(guò)期時(shí)間來(lái)實(shí)現(xiàn)。SETNX保證只有一個(gè)客戶(hù)端能成功設(shè)置鎖,過(guò)期時(shí)間防止死鎖。
立即學(xué)習(xí)“go語(yǔ)言免費(fèi)學(xué)習(xí)筆記(深入)”;
- 優(yōu)點(diǎn): 實(shí)現(xiàn)簡(jiǎn)單,性能高。
- 缺點(diǎn): 可能存在鎖過(guò)期時(shí)間不足導(dǎo)致鎖被提前釋放,或鎖續(xù)租機(jī)制復(fù)雜。需要考慮Redlock算法來(lái)提高可靠性,但Redlock本身也存在爭(zhēng)議。
import ( "github.com/go-redis/redis/v8" "context" "time" ) func acquireLock(ctx context.Context, rdb *redis.Client, lockKey string, lockValue string, expireTime time.Duration) (bool, error) { ok, err := rdb.SetNX(ctx, lockKey, lockValue, expireTime).Result() if err != nil { return false, err } return ok, nil } func releaseLock(ctx context.Context, rdb *redis.Client, lockKey string, lockValue string) error { // 使用lua腳本保證原子性 script := ` if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end ` result, err := rdb.Eval(ctx, script, []string{lockKey}, lockValue).Result() if err != nil { return err } if result == int64(1) { return nil } return nil // 鎖已被其他客戶(hù)端釋放 }
-
基于ZooKeeper的分布式鎖: 利用ZooKeeper的臨時(shí)順序節(jié)點(diǎn)來(lái)實(shí)現(xiàn)??蛻?hù)端創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),序號(hào)最小的節(jié)點(diǎn)獲得鎖。當(dāng)客戶(hù)端崩潰時(shí),臨時(shí)節(jié)點(diǎn)自動(dòng)刪除,釋放鎖。
- 優(yōu)點(diǎn): 可靠性高,鎖的釋放由ZooKeeper保證。
- 缺點(diǎn): 性能相對(duì)較低,實(shí)現(xiàn)復(fù)雜。需要引入ZooKeeper客戶(hù)端庫(kù)。
import ( "github.com/go-zookeeper/zk" "time" "sort" "strconv" ) func acquireLockZk(conn *zk.Conn, lockPath string, data []byte) (string, error) { // 創(chuàng)建臨時(shí)順序節(jié)點(diǎn) path, err := conn.Create(lockPath+"/", data, 0, zk.WorldACL(zk.PermAll)) if err != nil { return "", err } // 獲取所有子節(jié)點(diǎn) children, _, err := conn.Children(lockPath) if err != nil { return "", err } sort.Strings(children) // 判斷當(dāng)前節(jié)點(diǎn)是否是最小的節(jié)點(diǎn) if path == lockPath+"/"+children[0] { return path, nil // 獲得鎖 } // 監(jiān)聽(tīng)前一個(gè)節(jié)點(diǎn) index, _ := strconv.Atoi(path[len(lockPath)+1:]) prevIndex := index - 1 if prevIndex < 0 { prevIndex = 0 } prevNode := lockPath + "/" + children[prevIndex] _, _, ch, err := conn.GetW(prevNode) if err != nil { return "", err } <-ch // 阻塞等待前一個(gè)節(jié)點(diǎn)釋放 return acquireLockZk(conn, lockPath, data) // 遞歸嘗試獲取鎖 } func releaseLockZk(conn *zk.Conn, lockPath string) error { err := conn.Delete(lockPath, -1) return err }
-
基于Etcd的分布式鎖: 類(lèi)似于ZooKeeper,利用Etcd的lease機(jī)制和watch機(jī)制來(lái)實(shí)現(xiàn)。
- 優(yōu)點(diǎn): 相比ZooKeeper,Etcd性能更高,更易于部署。
- 缺點(diǎn): 實(shí)現(xiàn)復(fù)雜。需要引入Etcd客戶(hù)端庫(kù)。
import ( "context" "time" "github.com/coreos/etcd/clientv3" ) func acquireLockEtcd(cli *clientv3.Client, lockKey string, leaseTTL int64) (clientv3.LeaseID, error) { // 創(chuàng)建 lease resp, err := cli.Grant(context.TODO(), leaseTTL) if err != nil { return clientv3.LeaseID(0), err } // 嘗試獲取鎖 _, err = cli.Put(context.TODO(), lockKey, "locked", clientv3.WithLease(resp.ID)) if err != nil { return clientv3.LeaseID(0), err } // 續(xù)租,保持鎖 keepAliveChan, err := cli.KeepAlive(context.TODO(), resp.ID) if err != nil { return clientv3.LeaseID(0), err } go func() { for range keepAliveChan { } }() return resp.ID, nil } func releaseLockEtcd(cli *clientv3.Client, lockKey string, leaseID clientv3.LeaseID) error { _, err := cli.Revoke(context.TODO(), leaseID) return err }
如何選擇合適的分布式鎖方案?
選擇哪種方案取決于具體的業(yè)務(wù)場(chǎng)景。如果對(duì)性能要求較高,且可以容忍一定的錯(cuò)誤率,Redis是一個(gè)不錯(cuò)的選擇。如果對(duì)可靠性要求極高,ZooKeeper或Etcd更合適。
分布式鎖的續(xù)租機(jī)制是什么?為什么需要續(xù)租?
續(xù)租機(jī)制是為了防止鎖在任務(wù)執(zhí)行期間過(guò)期而導(dǎo)致鎖被其他客戶(hù)端獲取。當(dāng)客戶(hù)端獲得鎖后,會(huì)定期向鎖服務(wù)發(fā)送續(xù)租請(qǐng)求,延長(zhǎng)鎖的過(guò)期時(shí)間。
如何避免分布式鎖的誤刪除?
在使用Redis時(shí),為了避免客戶(hù)端A刪除了客戶(hù)端B的鎖,需要在設(shè)置鎖時(shí),設(shè)置一個(gè)唯一的value(例如UUID),釋放鎖時(shí),先判斷鎖的value是否與客戶(hù)端持有的value一致,只有一致時(shí)才刪除鎖??梢允褂肔ua腳本來(lái)保證判斷和刪除操作的原子性。