分布式锁:避免重复消息的处理

338 阅读4分钟

| 持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
单机情况下,可以使用sync.Mutex互斥锁来保证资源的互斥性,但是多机情况下访问共享资源时会出问题,因此需要使用分布式锁来避免重复消息的处理。 此次探讨一下分布式锁在处理重复消息的妙用。

dLock = lock.NewDLock(TaskLockPreStr+"_"+string(BookID))
type DLock struct {
   Key       string
   Value     string
   Retry     int
   Context   context.Context
   ExSeconds int64 // 超时时长
}
func NewDLock(context context.Context, key string) *DLock {
   timeStamp := _util.String(time.Now().Unix())
   return &DLock{
      Key:       key, // taskPreStr_bookid
      Value:     timeStamp,// 时间戳,确保唯一性
      Retry:     3,
      Context:   context,
      ExSeconds: 10, // 超时时间,避免崩溃时锁无法释放,默认10s
   }
}

  1. 初始化分布式锁。key是taskPreStr_bookid,value是该分布式锁初始化时的时间戳。
  2. setnx key value exseconds (setnx->set if not exist
  3. 逻辑执行
  4. del key(需要判断key和value是否对应,防止误删除
// 获取锁:
value, err := client.Get(dLock.Key)
if err != nil {
   return false, err
}

// 判断锁:
if value == "" || value != dLock.Value {
   return true, nil
}

// 释放锁: (主动释放)
err = client.Del(dLock.Key)

当出现错误时,需要释放锁,否则在重试的时候会被锁阻塞拦截。成功情况下锁自动超时释放。

问题

问题1 进程a、b、c、d同时竞争锁,若产生了死锁,如何解决?
给进程的锁设置超时时间:set(“lock”, 四元组, “NX”, “EX”, 30) ,比如进程A超时了,进程B就可以获取到锁。

问题2 来自问题1的设置超时解决方案:当A设置锁超时之后,B可以获取到锁,如果B还没执行完逻辑而A执行完后,A释放了B的锁,如何解决锁误解除的问题?
key唯一,就是避免锁的互相获取。超时释放或使用完毕后直接删除key即可。
问题3 来自问题1的设置超时解决方案:进程A超时但是任务还未完成,此时现场A可以继续完成任务,也不会误删其他锁,但是此时多个执行者同时执行临界区代码,难以保证数据一致性,如何解决?
在redisession中使用了看门狗机制,可以自动续约。
lock.lock(); 是阻塞式等待的,默认加锁时间是30s;如果业务超长,运行期间会自动续期到30s。不用担心业务时间长,锁自动过期被删掉;加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题; 也可以自己指定解锁时间lock.lock(10,TimeUnit.SECONDS),10秒钟自动解锁,自己指定解锁时间redis不会自动续期; 但是,其实在业务代码中不需要看门狗机制。因为在方法调用过程中会携带context上下文信息,上下文信息中会定义链路运行的超时时间,超时时间肯定是比锁的过期时间还要短的,所以不存在线程在锁过期时间内执行不完的情况。所以用不上看门狗机制。

分布式锁的其他实现

  1. 利用数据库的唯一索引来实现。唯一索引具有排他性,同一时刻只允许一个竞争者获取锁。加锁时在数据库中插入一条锁记录,利用业务id防重。当第一个竞争者加锁成功,第二个竞争者再来加锁就会抛出唯一索引冲突。抛出异常即加锁失败。加锁时插入一条记录,解锁时删除这条记录。

获得锁的进程挂了不释放锁:加锁前判断已存在锁记录的创建时间和当前时间之差是否超过锁的超时时间,超过则删除这条记录,插入新的记录。
单个数据库的单点问题:实现数据库的高可用方案。

  1. 利用zookeeper实现分布式锁。在zookeeper中创建临时顺序节点,利用节点的不能重复创建来保证排他性。
    临时顺序节点:根据创建的时间为节点编号,创建节点的客户端与zk断开连接后,临时节点会被删除。
    加锁过程:在zk中创建持久节点。当第一个客户端获取所,需要在持久节点下创建临时节点。之后客户端查找临时节点下所有临时节点并排序,判断自己的临时节点是不是顺序最靠前。第一个节点可以成功获取锁,其他节点处于等待状态,处于等待状态的节点监听前一个顺序的节点状态。
    解锁过程:当任务完成,客户端释放锁,就会调用删除节点的指令,如果任务执行时客户端崩溃,会断开和zk服务端的连接,也会自动删除节点。

参考

www.modb.pro/db/77169
blog.csdn.net/qq_42764269…