分布式锁可以保证当有多台实例同时竞争一把把锁?

分布式锁?选主?

分布式锁可以保证当有多台实例同时竞争一把锁时,只有一个人会成功,其他的都是失败。诸如共享资源修改、幂等、频控等场景都可以通过分布式锁来实现。

还有一种场景,也可以通过分布式锁来实现,那就是选主,为了保证服务的可用性,我们都会以一主多从的方式去部署,特别是提供存储能力的服务。Leader服务来接收数据的写入,然后将数据同步给Follower服务。当Leader服务挂掉时,我们需要从Follower服务中重新选举一个服务来当Leader,复杂的方式是通过Raft协议去协商,简单点,可以通过分布式锁的思路来做:

所有的Follower服务去竞争同一把锁,并给这个锁设置一个过期时间只会有一个Follower服务取到锁,这把锁的值就为它的标识,他就变成了Leader服务其他Follower服务竞争失败后,去获取锁得到的当前的Leader服务标识,与之通信Leader服务需要在锁过期之前不断的续期,证明自己是健康的所有Follower服务监控这把锁是否还被Leader服务持有,如果没有,就跳到了第1步

通过 Redis、Zookeeper 都可以实现,不过这次,我们使用 Etcd 来实现。

Etcd 简单介绍

Etcd:A highly-available key value store for shared configuration and service discovery。

Etcd 是一个K/V存储,和 Redis 功能类似,这是我对它的直观印象,和实现Master选举好像八竿子打不着。随着对 Etcd 了解的加深,我才开始对官网介绍那句话有了一定理解,Redis K/V 存储是用来做纯粹的缓存功能,高并发读写是核心,而 Etcd 这个基于 Raft 的分布式 K/V 存储,强一致性的 K/V 读写是核心,基本这点诞生了很多有想象力的使用场景:服务发现、分布式锁、Master 选举等等。

基于 Etcd 以下特性,我们可以实现自动选主:

准备工作启动 Etcd

我们使用 Docker 安装,简单方便:

> docker run -d --name Etcd-server 
    --publish 2379:2379 
    --publish 2380:2380 
    --env ALLOW_NONE_AUTHENTICATION=yes 
    --env ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379 
    bitnami/etcd:latest

最好是使用最新般本

Go 依赖库安装

Etcd 提供开箱即用的选主工作库,我们直接使用就行

> go get go.etcd.io/etcd/client/v3

这一步看似简单,如果放在以前,少不了一顿百度,原因是因为它依赖的 grpc 和 bbolt 库的版本不能是最新的,需要在 go.mod 中去写死版本。所幸赶上了好时代,官方终于出手整改了,现在只要一行命令行。

选主Demo

package main

图片[1]-分布式锁可以保证当有多台实例同时竞争一把把锁?-唐朝资源网

import ( "context" "flag" "fmt" "os" "os/signal" "time" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/client/v3/concurrency" ) var ( serverName = flag.String("name", "", "") ) func main() { flag.Parse() // Etcd 服务器地址 endpoints := []string{"127.0.0.1:2379"} clientConfig := clientv3.Config{ Endpoints: endpoints, DialTimeout: 2 * time.Second, } cli, err := clientv3.New(clientConfig)

图片[2]-分布式锁可以保证当有多台实例同时竞争一把把锁?-唐朝资源网

if err != nil { panic(err) } s1, err := concurrency.NewSession(cli) if err != nil { panic(err) } fmt.Println("session lessId is ", s1.Lease()) e1 := concurrency.NewElection(s1, "my-election") go func() { // 参与选举,如果选举成功,会定时续期 if err := e1.Campaign(context.Background(), *serverName); err != nil { fmt.Println(err) } }() masterName := "" go func() { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() timer := time.NewTicker(time.Second) for range timer.C { timer.Reset(time.Second) select { case resp := 0 { // 查看当前谁是 master masterName = string(resp.Kvs[0].Value) fmt.Println("get master with:", masterName) } } } }() go func() { timer := time.NewTicker(5 * time.Second) for range timer.C { // 判断自己是 master 还是 slave if masterName == *serverName { fmt.Println("oh, i'm master") } else { fmt.Println("slave!") } } }() c := make(chan os.Signal, 1) // 接收 Ctrl C 中断 signal.Notify(c, os.Interrupt, os.Kill)

图片[3]-分布式锁可以保证当有多台实例同时竞争一把把锁?-唐朝资源网

s := <-c fmt.Println("Got signal:", s) e1.Resign(context.TODO()) }

我们在两个终端分别运行下面两个命令,模拟两个服务去竞争:

> go run main.go -name A
session lessId is  7587863771971134868
get master with: A
get master with: A
get master with: A
get master with: A
oh, i'm master

> go run main.go -name B
session lessId is  7587863771971134876
get master with: A
get master with: A
get master with: A
get master with: A
slave!

当我们使用 Ctrl C 中断,此时 B 就成为了 master

> go run main.go -name A
session lessId is  7587863771971134868
get master with: A

get master with: A
get master with: A
get master with: A
oh, i'm master
^CGot signal: interrupt

> go run main.go -name B
session lessId is  7587863771971134876
get master with: A
get master with: A
get master with: A
get master with: A
slave!
get master with: B
get master with: B
get master with: B
get master with: B
oh, i'm master

原理

当我们启动 A 和 B 两个服务时,他们后会在公共前缀 “my-election/” 下创建自己的 key,这个 key 的构成为 “my-election/” + 十六进制(LessId)。这个LessId 是在服务启动时,从 Etcd 服务端取到的客户端唯一标识。比如上面程序运行的两个服务创建的 key 分别是:

因为是通过事务的方式去创建 key,可以保证如果这个 key 已经创建了,不去创建了。并且这个 key 是有过期时间,两个服务 A 和 B 会启动一个协程定期去刷新过期时间,通过这个方式证明自己的健康的。

现在两个服务都创建了 key, 那么那个才是 master 呢?我们选取最早创建的那个 key 的拥有者作为 master。

Etcd 服务的查询接口支持根据前缀查询和按照创建时间排序,所以我们可以轻松的拿到第一个创建成功的 key,这个 key 对应的值就是 master 了,也就是 A 服务。

当现在 master 服务挂掉了,因为它的 key 没有在过期之前续期,就会被删除的,此时当初第二个创建的 key 就会变成第一个,那个 master 就变成了 B 服务。

我们是通过e1.Campaign(context.Background(), *serverName)行代码是参加去参加选举的,里面有一个细节:如果竞争失败,这个函数会阻塞,直到它选举成功或者服务中断。也就是说:如果 B 服务创建的 key

不是最早的一个,那它会一直等待,直到服务 A 的 key 被删除后,函数才会有返回。

© 版权声明
THE END
喜欢就支持一下吧
点赞256赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容