etcd实现分布式锁
转载自:etcd实现分布式锁
当并发的访问共享资源的时候,如果没有加锁的话,无法保证共享资源安全性和正确性。这个时候就需要用到锁
1、需要具备的特性
- 需要保证互斥访问(分布式环境需要保证不同节点、不同线程的互斥访问)
- 需要有超时机制,防止锁意外未释放,其他节点无法获取到锁;也要保证任务能够正常执行完成,不能超时了任务还没结束,导致任务执行一般被释放锁
- 需要有阻塞和非阻塞两种请求锁的接口
2、本地锁
当业务执行在同一个线程内,也就是我初始化一个本地锁,其他请求也认这把锁。一般是服务部署在单机环境下。
我们可以看下下面的例子,开1000个goroutine并发的给Counter做自增操作,结果会是什么样的呢?
package main
import (
"fmt"
"sync"
)
var sg sync.WaitGroup
type Counter struct {
count int
}
// 自增操作
func (m *Counter) Incr() {
m.count++
}
// 获取总数
func (m *Counter) Count() int {
return m.count
}
func main() {
c := &Counter{}
for i := 0; i < 1000; i++ {
sg.Add(1)
// 模拟并发请求
go func() {
c.Incr()
sg.Done()
}()
}
sg.Wait()
fmt.Println(c.Count())
}
结果是count的数量并不是预想中的1000,而是下面这样,每次打印出的结果都不一样,但是接近1000
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
953
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
982
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
984
出现这个问题的原因就是没有给自增操作加锁
下面我们修改代码如下,在Incr中加上go的mutex互斥锁
package main
import (
"fmt"
"sync"
)
var sg sync.WaitGroup
type Counter struct {
count int
mu sync.Mutex
}
func (m *Counter) Incr() {
// 每次写之前先加锁,写完之后释放锁
m.mu.Lock()
defer m.mu.Unlock()
m.count++
}
func (m *Counter) Count() int {
return m.count
}
func main() {
c := &Counter{}
for i := 0; i < 1000; i++ {
sg.Add(1)
go func() {
c.Incr()
sg.Done()
}()
}
sg.Wait()
fmt.Println(c.Count())
}
可以看到现在count正常输出1000了
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
1000
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
1000
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
1000
3、etcd分布式锁
简单部署一个etcd集群
├── docker-compose.yml
├── etcd
│ └── Dockerfile
Dockerfile文件内容
FROM bitnami/etcd:latest
LABEL maintainer="liuyuede123 <liufutianoppo@163.com>"
Docker-compose.yml内容
version: '3.5'
# 网络配置
networks:
backend:
driver: bridge
# 服务容器配置
services:
etcd1: # 自定义容器名称
build:
context: etcd # 指定构建使用的 Dockerfile 文件
environment:
- TZ=Asia/Shanghai
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd1
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd1:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd1:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
ports: # 设置端口映射
- "12379:2379"
- "12380:2380"
networks:
- backend
restart: always
etcd2: # 自定义容器名称
build:
context: etcd # 指定构建使用的 Dockerfile 文件
environment:
- TZ=Asia/Shanghai
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd2
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd2:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd2:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
ports: # 设置端口映射
- "22379:2379"
- "22380:2380"
networks:
- backend
restart: always
etcd3: # 自定义容器名称
build:
context: etcd # 指定构建使用的 Dockerfile 文件
environment:
- TZ=Asia/Shanghai
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd3
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd3:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd3:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
ports: # 设置端口映射
- "32379:2379"
- "32380:2380"
networks:
- backend
restart: always
执行docker-compose up -d启动etcd服务,可以看到docker中已经启动了3个服务

实现互斥访问
package main
import (
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"sync"
)
var sg sync.WaitGroup
type Counter struct {
count int
}
func (m *Counter) Incr() {
m.count++
}
func (m *Counter) Count() int {
return m.count
}
func main() {
endpoints := []string{"http://127.0.0.1:12379", "http://127.0.0.1:22379", "http://127.0.0.1:32379"}
// 初始化etcd客户端
client, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
fmt.Println(err)
return
}
defer client.Close()
counter := &Counter{}
sg.Add(100)
for i := 0; i < 100; i++ {
go func() {
// 这里会生成租约,默认是60秒
session, err := concurrency.NewSession(client)
if err != nil {
panic(err)
}
defer session.Close()
locker := concurrency.NewLocker(session, "/my-test-lock")
locker.Lock()
counter.Incr()
locker.Unlock()
sg.Done()
}()
}
sg.Wait()
fmt.Println("count:", counter.Count())
}
执行结果:
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
count: 100
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
count: 100
user@userdeMacBook-Pro ~/go/src/go-demo/mutex go run main.go
count: 100
实现超时机制
当某个客户端持有锁时,由于某些原因导致锁未释放,就会导致这个客户端一直持有这把锁,其他客户端一直获取不到锁。所以需要分布式锁实现超时机制,当锁未释放时,会因为etcd的租约会到期而释放锁。当业务正常处理时,租约到期之前会继续续约,知道业务处理完毕释放锁。
package main
import (
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"sync"
"time"
)
var sg sync.WaitGroup
type Counter struct {
count int
}
func (m *Counter) Incr() {
m.count++
}
func (m *Counter) Count() int {
return m.count
}
func main() {
endpoints := []string{"http://127.0.0.1:12379", "http://127.0.0.1:22379", "http://127.0.0.1:32379"}
client, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
fmt.Println(err)
return
}
defer client.Close()
counter := &Counter{}
session, err := concurrency.NewSession(client)
if err != nil {
panic(err)
}
defer session.Close()
locker := concurrency.NewLocker(session, "/my-test-lock")
fmt.Println("locking...", time.Now().Format("2006-01-02 15:04:05"))
locker.Lock()
fmt.Println("locked...", time.Now().Format("2006-01-02 15:04:05"))
// 模拟业务
time.Sleep(100 * time.Second)
counter.Incr()
locker.Unlock()
fmt.Println("released...", time.Now().Format("2006-01-02 15:04:05"))
fmt.Println("count:", counter.Count())
}
命令行开2个窗口,第一个窗口执行程序并获取锁,之后模拟意外退出并没有调用unlock方法
go run main.go
locking... 2022-09-03 23:41:48 # 租约生成时间
locked... 2022-09-03 23:41:48
^Csignal: interrupt
第二个窗口,在第一个窗口退出之前尝试获取锁,此时是阻塞状态。第一个窗口退出之后由于租约还没到期,第二个窗口还是获取锁的状态。等到第一个窗口租约到期(默认60秒),第二个获取锁成功
locking... 2022-09-03 23:41:52
locked... 2022-09-03 23:42:48 # 第一个租约60秒到期,获取锁成功
released... 2022-09-03 23:44:28
count: 1
实现阻塞和非阻塞接口
上面的例子中已经实现了阻塞接口,即当前有获取到锁的请求,则其他请求阻塞等待锁释放
非阻塞的方式就是尝试获取锁,如果失败立即返回。etcd中是实现了tryLock方法
// TryLock locks the mutex if not already locked by another session.
// If lock is held by another session, return immediately after attempting necessary cleanup
// The ctx argument is used for the sending/receiving Txn RPC.
func (m *Mutex) TryLock(ctx context.Context) error {
具体看下面的例子
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"sync"
"time"
)
var sg sync.WaitGroup
type Counter struct {
count int
}
func (m *Counter) Incr() {
m.count++
}
func (m *Counter) Count() int {
return m.count
}
func main() {
endpoints := []string{"http://127.0.0.1:12379", "http://127.0.0.1:22379", "http://127.0.0.1:32379"}
client, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
fmt.Println(err)
return
}
defer client.Close()
counter := &Counter{}
session, err := concurrency.NewSession(client)
if err != nil {
panic(err)
}
defer session.Close()
// 此处使用newMutex初始化
locker := concurrency.NewMutex(session, "/my-test-lock")
fmt.Println("locking...", time.Now().Format("2006-01-02 15:04:05"))
err = locker.TryLock(context.Background())
// 获取锁失败就抛错
if err != nil {
fmt.Println("lock failed", err)
return
}
fmt.Println("locked...", time.Now().Format("2006-01-02 15:04:05"))
time.Sleep(100 * time.Second)
counter.Incr()
err = locker.Unlock(context.Background())
if err != nil {
fmt.Println("unlock failed", err)
return
}
fmt.Println("released...", time.Now().Format("2006-01-02 15:04:05"))
fmt.Println("count:", counter.Count())
}
窗口1、窗口2执行结果
go run main.go
locking... 2022-09-04 00:00:21
locked... 2022-09-04 00:00:21
released... 2022-09-04 00:02:01
count: 1
go run main.go
locking... 2022-09-04 00:00:27
lock failed mutex: Locked by another session
etcd实现分布式锁的更多相关文章
- 一次基于etcd的分布式锁自动延时失败问题的排查
今天在测试基于etcd的分布式锁过程中,在测试获取锁后,释放之前超出TTL时长的情况下自动延长TTL这部分功能,在延长指定key的TTL时总是返回404错误信息,在对目标KEY更新TTL时目标KEY已 ...
- golang基于etcd实现分布式锁(转)
下面描述使用 Etcd 实现分布式锁的业务流程,假设对某个共享资源设置的锁名为:/lock/mylock 步骤 1: 准备 客户端连接 Etcd,以 /lock/mylock 为前缀创建全局唯一的 k ...
- golang使用etcd实现分布式锁
package main import ( "context" "fmt" "time" "go.etcd.io/etcd/cli ...
- 基于go+etcd实现分布式锁
原文链接:https://www.yii-china.com/topic/detail/113 package main import ( "context" "fmt& ...
- ETCD分布式锁实现选主机制(Golang实现)
ETCD分布式锁实现选主机制(Golang) 为什么要写这篇文章 做架构的时候,涉及到系统的一个功能,有一个服务必须在指定的节点执行,并且需要有个节点来做任务分发,想了半天,那就搞个主节点做这事呗,所 ...
- etcd分布式锁及事务
前言 分布式锁是控制分布式系统之间同步访问共享资源的一种方式.在分布式系统中,常常需要协调他们的动作.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互 ...
- Redis 中的原子操作(3)-使用Redis实现分布式锁
Redis 中的分布式锁如何使用 分布式锁的使用场景 使用 Redis 来实现分布式锁 使用 set key value px milliseconds nx 实现 SETNX+Lua 实现 使用 R ...
- 分布式ID生成器及redis,etcd分布式锁
分布式id生成器 有时我们需要能够生成类似MySQL自增ID这样不断增大,同时又不会重复的id.以支持业务中的高并发场景.比较典型的,电商促销时,短时间内会有大量的订单涌入到系统,比如每秒10w+.明 ...
- Etcd 使用场景:通过分布式锁思路实现自动选主
分布式锁?选主? 分布式锁可以保证当有多台实例同时竞争一把锁时,只有一个人会成功,其他的都是失败.诸如共享资源修改.幂等.频控等场景都可以通过分布式锁来实现. 还有一种场景,也可以通过分布式锁来实现, ...
随机推荐
- DolphinScheduler 荣获 2021 中国开源云联盟优秀开源项目奖!
点击上方 蓝字关注我们 好消息,中国开源云联盟(China Open Source Cloud League,简称"COSCL")于近日公布 2021 杰出开源贡献者.优秀开源项目 ...
- 羽夏看Linux内核——引导启动(下)
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作.如想转载,请把我的转载信息附在文章后面,并 ...
- mybatisplus-Service CRUD 接口
通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前 ...
- 在cmd中使用doskey来实现alias别名功能
作为一枚网络工程师,经常就是面对一堆黑框框,也是就是终端.不同操作系统.不同厂家的目录,功能相同但是键入的命令又大不相同,这些差异化容易让脑子混乱.比如华为.思科.H3C.锐捷的设备, ...
- js实现多条件排序
1.sort排序允许接受一个参数(函数),这个函数接受2个形参a,b,并且通过冒泡的方式比较.例子:Arr.sort((a,b) => a-b); 升序排列Arr.sort((a,b) =& ...
- TCP/UDP报文格式
TCP报文格式 源端口:数据发送方的端口号 目的端口:数据接收方的端口号 序号:本数据报文中的第一个字节的序号(在数据流中每个字节都对应一个序号) 确认号:希望收到的下一个数据报文中的第一个字节的序号 ...
- 从零教你使用MindStudio进行Pytorch离线推理全流程
摘要:MindStudio的是一套基于华为自研昇腾AI处理器开发的AI全栈开发工具平台,该IDE上功能很多,涵盖面广,可以进行包括网络模型训练.移植.应用开发.推理运行及自定义算子开发等多种任务. 本 ...
- 开发个RTMP播放器居然这么难?RTMP播放器对标和考察指标
好多开发者提到,RTMP播放器,不知道有哪些对标和考察指标,以下大概聊聊我们的一点经验,感兴趣的,可以关注 github: 1. 低延迟:大多数RTMP的播放都面向直播场景,如果延迟过大,严重影响体验 ...
- KingbaseES R6 集群repmgr witness 手工配置案例
使用见证服务器: 见证服务器是一个正常的KingbaseES实例,不是流复制群集的一部分; 其目的是,如果发生故障转移情况,则提供证明它是主服务器本身不可用的证据,而不是例如在不同物理位置之间的网络分 ...
- Linux_etc-shadow文件总结
shadow 文件权限 $ll shadow ---------- 1 root root 1131 Aug 6 12:04 shadow 可以看出只有root可以操作它,普通用户执行passwd,内 ...