go 分布式锁的几种使用方案

23 阅读2分钟

1 基于 Redis 实现的分布式锁

如果要使用更复杂精细的功能,可以使用redis官方提供的go实现的分布式锁redsync(github.com/go-redsync/… ),其特性有如下几点:

  1. value值的随机性,唯一性验证,防误删
  2. 预估业务可执行时间,防获取无效锁
  3. 重试机制,提高获取锁的效率
  4. 多redis节点支持,保证高可用性(Redlock)

缺点:不支持可重入性

2 基于数据库实现的分布式锁

特点:

  1. 互斥,同一时间内只有一个线程持有
  2. 超时机制,线程获取锁后异常退出,超时后将释放
  3. 防止误释放,锁会记录持有者

基本使用:

m := sql.NewMutex(db)

// 不记录持有者
m.Lock(key, time.Second*3)
m.Unlock(key)

// 记录持有者
m.LockWithHolder(key, "uuid", time.Second)
m.UnlockWithHolder(key, "uuid")

实现:

  1. 锁的结构
type Lock struct {
    Id       uint      `gorm:"column:id;primaryKey"`
    Key      string    `gorm:"column:key;type:varchar(255);unique_index"`
    Holder   string    `gorm:"column:holder;type:varchar(255)"`
    ExpireAt time.Time `gorm:"column:expire_at;type:datetime"`
}

type Mutex struct {
    db *gorm.DB
}

func NewMutex(db *gorm.DB) *Mutex {
    return &Mutex{
       db: db,
    }
}
  1. 加锁
func (m *Mutex) Lock(key string, expiration time.Duration) error {
    return m.LockWithHolder(key, "default", expiration)
}

func (m *Mutex) LockWithHolder(key string, holder string, expiration time.Duration) error {
    err := m.db.Transaction(func(tx *gorm.DB) error {
       now := time.Now()
       expireAt := now.Add(expiration)
       lock := &Lock{}

       // 查询锁是否存在
       err := tx.Where("`key` = ?", key).First(lock).Error
       if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
          return err
       }

       if lock.Id == 0 {
          // 锁不存在则创建
          err = tx.Create(&Lock{Key: key, ExpireAt: expireAt, Holder: holder}).Error
          if err != nil {
             return err
          }
          return nil
       } else if lock.ExpireAt.Before(now) {
          // 锁存在并且过期
          err = tx.Model(&Lock{}).Where("`key` = ?", key).Updates(map[string]interface{}{"expire_at": expireAt, "holder": holder}).Error
          if err != nil {
             return err
          }
          return nil
       }
       return errors.New("locked")
    })

    if err != nil {
       fmt.Println(err)
    }
    return err
}
  1. 解锁
func (m *Mutex) Unlock(key string) error {
    return m.UnlockWithHolder(key, "default")
}

func (m *Mutex) UnlockWithHolder(key string, holder string) error {
    err := m.db.Transaction(func(tx *gorm.DB) error {
       lock := &Lock{}
       now := time.Now()

       err := tx.Where("`key` = ?", key).First(lock).Error
       if err != nil {
          if errors.Is(err, gorm.ErrRecordNotFound) {
             return nil
          }
          return err
       }

       if lock.ExpireAt.Before(now) {
          err := tx.Where("`key` = ?", key).Delete(&Lock{}).Error
          if err != nil {
             return err
          }
          return nil
       } else if lock.Holder == holder {
          err := tx.Where("`key` = ?", key).Delete(&Lock{}).Error
          if err != nil {
             return err
          }
          return nil
       }
       return errors.New("lock is held by other")
    })
    if err != nil {
       fmt.Println("Unlock", err)
    }
    return nil
}

3 基于 etcd 实现的分布式锁

由于etcd官方已经提供了基于etcd的分布式锁的实现,所以可以直接使用,具体实现在下面的包中。

github.com/etcd-io/etcd/client/v3/concurrency

基本使用:

// 初始化客户端
cli, _ := clientv3.New(clientv3.Config{
    Endpoints:   []string{"127.0.0.1:2379"},
    DialTimeout: 5 * time.Second,
})

// 新建一次会话,内部逻辑会申请一笔租约,并进行续约
session, _ := concurrency.NewSession(cli)

// 新建一个锁
m := concurrency.NewMutex(session, "key")

// 阻塞加锁
m.Lock(context.TODO())

// 非阻塞加锁
m.TryLock(context.TODO())

// 解锁
m.Unlock(context.TODO())

4 方案对比

分布式锁特性code-go现有redsync数据库etcd
互斥支持支持支持支持
超时机制支持支持支持支持
完备锁接口(支持阻塞与非阻塞)只支持非阻塞支持只支持非阻塞支持
可重入性支持
公平性支持
单节点故障容错支持支持