转载自:etcd实现分布式锁

当并发的访问共享资源的时候,如果没有加锁的话,无法保证共享资源安全性和正确性。这个时候就需要用到锁

1、需要具备的特性

  1. 需要保证互斥访问(分布式环境需要保证不同节点、不同线程的互斥访问)
  2. 需要有超时机制,防止锁意外未释放,其他节点无法获取到锁;也要保证任务能够正常执行完成,不能超时了任务还没结束,导致任务执行一般被释放锁
  3. 需要有阻塞和非阻塞两种请求锁的接口

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实现分布式锁的更多相关文章

  1. 一次基于etcd的分布式锁自动延时失败问题的排查

    今天在测试基于etcd的分布式锁过程中,在测试获取锁后,释放之前超出TTL时长的情况下自动延长TTL这部分功能,在延长指定key的TTL时总是返回404错误信息,在对目标KEY更新TTL时目标KEY已 ...

  2. golang基于etcd实现分布式锁(转)

    下面描述使用 Etcd 实现分布式锁的业务流程,假设对某个共享资源设置的锁名为:/lock/mylock 步骤 1: 准备 客户端连接 Etcd,以 /lock/mylock 为前缀创建全局唯一的 k ...

  3. golang使用etcd实现分布式锁

    package main import ( "context" "fmt" "time" "go.etcd.io/etcd/cli ...

  4. 基于go+etcd实现分布式锁

    原文链接:https://www.yii-china.com/topic/detail/113 package main import ( "context" "fmt& ...

  5. ETCD分布式锁实现选主机制(Golang实现)

    ETCD分布式锁实现选主机制(Golang) 为什么要写这篇文章 做架构的时候,涉及到系统的一个功能,有一个服务必须在指定的节点执行,并且需要有个节点来做任务分发,想了半天,那就搞个主节点做这事呗,所 ...

  6. etcd分布式锁及事务

    前言 分布式锁是控制分布式系统之间同步访问共享资源的一种方式.在分布式系统中,常常需要协调他们的动作.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互 ...

  7. Redis 中的原子操作(3)-使用Redis实现分布式锁

    Redis 中的分布式锁如何使用 分布式锁的使用场景 使用 Redis 来实现分布式锁 使用 set key value px milliseconds nx 实现 SETNX+Lua 实现 使用 R ...

  8. 分布式ID生成器及redis,etcd分布式锁

    分布式id生成器 有时我们需要能够生成类似MySQL自增ID这样不断增大,同时又不会重复的id.以支持业务中的高并发场景.比较典型的,电商促销时,短时间内会有大量的订单涌入到系统,比如每秒10w+.明 ...

  9. Etcd 使用场景:通过分布式锁思路实现自动选主

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

随机推荐

  1. 走进Redis:哨兵集群

    为什么需要哨兵 在 Redis 的主从库模式中,如果从库发生了故障,用户的操作是可以继续进行的,因为写操作是只在主库中进行的.那么,如果主库发生了故障,用户的操作将会收到影响.这时候可能会需要选择一个 ...

  2. Babylon.js 入门简介和开发实例

    Babylon.js是一款WebGL开发框架,和Three.js类似. Three.js是由社区推动的,比Babylon.js要成熟些,而Babylon.js是微软推动的,和微软的相关技术结合更好. ...

  3. 面试突击74:properties和yml有什么区别?

    properties 和 yml 都是 Spring Boot 支持的两种配置文件,它们可以看作是 Spring Boot 在不同时期的两款"产品".在 Spring Boot 时 ...

  4. ceph 010 clustermap ceph调优

    cluster map [ceph: root@clienta /]# ceph mon dump epoch 4 fsid 2ae6d05a-229a-11ec-925e-52540000fa0c ...

  5. CCF NOI Online 2021 提高组 T2 积木小赛 (子序列自动机+后缀自动机,O(n^2))

    题面 Alice 和 Bob 最近热衷于玩一个游戏--积木小赛. Alice 和 Bob 初始时各有 n 块积木从左至右排成一排,每块积木都被标上了一个英文小写字母. Alice 可以从自己的积木中丢 ...

  6. C#基础_枚举

    一.在学习枚举之前,首先来听听枚举的优点. 1.枚举能够使代码更加清晰,它允许使用描述性的名称表示整数值. 2.枚举使代码更易于维护,有助于确保给变量指定合法的.期望的值. 3.枚举使代码更易输入. ...

  7. 面试现场!月薪3w+的这些数据挖掘SQL面试题你都掌握了吗? ⛵

    作者:韩信子@ShowMeAI 数据分析实战系列:https://www.showmeai.tech/tutorials/40 AI 面试题库系列:https://www.showmeai.tech/ ...

  8. [CISCN2019 华北赛区 Day1 Web2]ikun-1|python反序列化

    考点:JWT身份伪造.python pickle反序列化.逻辑漏洞 1.打开之后首页界面直接看到了提示信息,信息如下: 2.那就随便注册一个账号进行登录,然后购买lv6,但是未发现lv6,那就查看下一 ...

  9. Dapr中国社区活动之 分布式运行时开发者日 (2022.09.03)

    自2019年10月首次发布以来,Dapr(Distributed Application Runtime,分布式应用运行时)因其"更稳定"."更可靠".&quo ...

  10. 全局索引与分区索引对于SQL性能影响的比较

    KingbaseES 提供了对于分区表 global index 的支持.global index 不仅提供了对于唯一索引功能的改进(无需包含分区键),而且在性能上相比非global index (l ...