基于 etcd 实现的分布式锁

etcd 的分布式锁结构?

  • Session 用于标识 etcd 与客户端的连接,每一个 Session 都有一个唯一的 LeaseID 来实现租约机制
  • Mutex 通过客户端传入的 pfx 标识同一把分布式锁,
  • 使用 pfx + LeaseID 得到 myKey 标识持有该锁的客户端
  • 使用 myRev 标识这个前缀 pfx 下的 revision 版本号(revision 在每次用户修改数据时都会递增)
go 复制代码
// NewLocker creates a sync.Locker backed by an etcd mutex.
func NewLocker(s *Session, pfx string) sync.Locker {
   return &lockerMutex{NewMutex(s, pfx)}
}

// Mutex implements the sync Locker interface with etcd
type Mutex struct {
   s *Session

   pfx   string 
   myKey string 
   myRev int64
   hdr   *pb.ResponseHeader
}

// Session represents a lease kept alive for the lifetime of a client.
// Fault-tolerant applications may use sessions to reason about liveness.
type Session struct {
   client *v3.Client
   opts   *sessionOptions
   id     v3.LeaseID

   cancel context.CancelFunc
   donec  <-chan struct{}
}

etcd 如何实现加锁?

加锁流程主要是以下步:

  1. 设置 myKey 到 etcd 服务端,从 response 中得到一个版本号 revesion,设为 myRev
  2. 从response中还可以获得该前缀存在的最小版本号以及 put myKey 时该前缀是否存在。如果当前 myKey 前缀不存在,或者当前最小的版本号就是自身 myRev,表明获取到了锁,直接返回;否则阻塞等待前一个版本号即 myRev - 1 的 key 被删除
  3. 被唤醒之后需要再次判断 myRev 是不是最小的版本号,如果是则获取锁成功,否则执行继续监听上上一个 revision (等待队列中 myRev 从小到大排列,每个客户端只监听 myRev - 1 是否被删除;可能存在队列中间的客户端删除了 myKey 唤醒了后一个客户端,但是这个进程的 myRev 不是当前最小,对于后一个客户端来说需要监听被删除 myKey 的上一个 Revision )

如何保证加锁后锁一定能释放?

  • etcd 提供了租约机制实现锁的 TTL
  • 同时 etcd 还提供了续约机制,保证业务执行过程中,锁不会被释放

etcd 是如何避免惊群效应的 ?

  • 通过 revision 排队,每一个进程只 watch myRev-1 的删除事件
go 复制代码
// Lock locks the mutex with a cancelable context. If the context is canceled
// while trying to acquire the lock, the mutex tries to clean its stale lock entry.
func (m *Mutex) Lock(ctx context.Context) error {
   resp, err := m.tryAcquire(ctx)
   if err != nil {
      return err
   }
   // if no key on prefix / the minimum rev is key, already hold the lock
   ownerKey := resp.Responses[1].GetResponseRange().Kvs
   if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
      m.hdr = resp.Header
      return nil
   }
   client := m.s.Client()
   // wait for deletion revisions prior to myKey
   // TODO: early termination if the session key is deleted before other session keys with smaller revisions.
   _, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
   // release lock key if wait failed
   if werr != nil {
      m.Unlock(client.Ctx())
      return werr
   }

   // make sure the session is not expired, and the owner key still exists.
   gresp, werr := client.Get(ctx, m.myKey)
   if werr != nil {
      m.Unlock(client.Ctx())
      return werr
   }

   if len(gresp.Kvs) == 0 { // is the session key lost?
      return ErrSessionExpired
   }
   m.hdr = gresp.Header

   return nil
}

etcd 如何实现解锁?

因为加锁时每个客户端的 myKey 都不相同,并且加锁的客户端实现了 watch 操作,所以解锁时,直接删除 myKey 即可。

go 复制代码
func (m *Mutex) Unlock(ctx context.Context) error {
   client := m.s.Client()
   if _, err := client.Delete(ctx, m.myKey); err != nil {
      return err
   }
   m.myKey = "\x00"
   m.myRev = -1
   return nil
}
相关推荐
Msshu1232 天前
快充诱骗取电芯片XSP26A 支持全协议取电+串口发送充电器功率+共用D+D-网络
mongodb·postgresql·etcd·flume·consul·kylin
姚不倒2 天前
etcd 学习系列(四):存储模型 —— WAL 和 Snapshot 是如何配合的
运维·etcd
姚不倒2 天前
etcd 学习系列(二):集群架构 —— 3 节点是如何工作的
运维·架构·etcd
姚不倒3 天前
etcd 学习系列(七):排障指南 —— 常见问题与解决方案
运维·etcd
姚不倒3 天前
etcd 学习系列(一):从业务需求出发,理解 etcd 是什么
运维·数据库·etcd
Asum1ta7 天前
生产环境 Kubernetes 高可用集群部署实战:外部 etcd + Keepalived + HAProxy + 多 Master 架构
架构·kubernetes·etcd
羑悻的小杀马特10 天前
百度ASR+BRPC+ETCD高并发高可用架构搭建,C++客户端API封装+语音识别子服务注册发现+全链路测试调优终极指南!
架构·sdk·etcd·cmake·百度智能云·brpc·语音识别子服务
harmony&11 天前
ETCD 存储系统实战:从单节点部署到 Kubernetes 高可用集群
数据库·kubernetes·etcd
忧郁的橙子.14 天前
深入浅出etcd与Raft:从核心组件到数据一致性原理
数据库·etcd
2601_9609067217 天前
Anthropic把Claude Code 2.1.236及以上版本的Fable 5会话
人工智能·kafka·时序数据库·etcd·tdengine