分布式锁
分布式锁广泛运用于分布式系统,目前常用的有:
- 进程内加锁
- trylock
- 基于redis setnx
- 基于zookeeper
- 基于etcd
进程内加锁
  var wg sync.WaitGroup
  var lock sync.Mutex
  for i := 0; i < 1000; i++ {
      wg.Add(1)
      go func() {
          defer wg.Done()
          lock.Lock()
          counter++
          lock.Unlock()
      }()
  }
  wg.Wait()
  println(counter)
trylock
在某些场景,我们只希望一个任务有单一的执行者,而不像计数器一样,所有的 Goroutine 都成功执行。后续的 Goroutine 在抢锁失败后,需要放弃执行,这时候就需要尝试加锁 / trylock。
尝试加锁,在加锁成功后执行后续流程,失败时不可以阻塞,而是直接返回加锁的结果。
在 Go 语言中可以用大小为 1 的 Channel 来模拟 trylock:
// Lock try lock
type Lock struct {
    c chan struct{}
}
// Lock try lock, return lock result
func (l Lock) Lock() bool {
    result := false
    select {
    case <-l.c:
        result = true
    default:
    }
    return result
}
// Unlock the try lock
func (l Lock) Unlock() {
    l.c <- struct{}{}
}
// NewLock generate a try lock
func NewLock() Lock {
    var l Lock
    l.c = make(chan struct{}, 1)
    l.c <- struct{}{}
    return l
}
func main() {
    var lock = NewLock()
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            if !lock.Lock() {
                println("lock failed")
                return
            }
            counter++
            println("current counter: ", counter)
            lock.Unlock()
        }()
    }
    wg.Wait()
每个 Goruntine 只有成功执行了Lock()才会继续执行后续代码,因此在Unlock()时可以保证Lock结构体里的 Channel 一定是空的,所以不会阻塞也不会失败。
在单机系统中,trylock 并不是一个好选择,因为大量的 Goruntine 抢锁会无意义地占用 cpu 资源,这就是活锁。
活锁指是的程序看起来在正常执行,但 cpu 周期被浪费在抢锁而非执行任务上,从而程序整体的执行效率低下。活锁的问题定位起来很麻烦,所以在单机场景下,不建议使用这种锁。
Redis 的setnx
在分布式场景中,也需要“抢占”的逻辑,可以用 Redis 的setnx实现
import (
    "github.com/go-redis/redis"
)
func setnx() {
    client := redis.NewClient(&redis.Options{})
    var lockKey = "counter_lock"
    var counterKey = "counter"
    // lock
    resp := client.SetNX(lockKey, 1, time.Second*5)
    lockStatus, err := resp.Result()
    if err != nil || !lockStatus {
        println("lock failed")
        return
    }
    // counter++
    getResp := client.Get(counterKey)
    cntValue, err := getResp.Int64()
    if err == nil || err == redis.Nil {
        cntValue++
        resp := client.Set(counterKey, cntValue, 0)
        _, err := resp.Result()
        if err != nil {
            println(err)
        }
    }
    println("current counter is ", cntValue)
    // unlock
    delResp := client.Del(lockKey)
    unlockStatus, err := delResp.Result()
    if err == nil && unlockStatus > 0 {
        println("unlock success")
    } else {
        println("unlock failed", err)
    }
}
func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            setnx()
        }()
    }
    wg.Wait()
}
通过代码和执行结果可以看到,远程调用setnx运行流程上和单机的 trolock非常相似,如果获取锁失败,那么相关的任务逻辑就不会继续向后执行。
setnx很适合高并发场景下用来争抢一些“唯一”的资源。比如交易摄合系统中卖家发起订单,多个买家会对其进行并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不同设备的时间不能保证使用的是统一的时间,也就不能保证时序。
所以,我们需要依赖于这些请求到达redis 节点的顺序来做正确的抢锁操作。
如果用户的网络环境比较差,是必然抢不到的。
基于 ZooKeeper
基于ZooKeeper 的锁与基于 Redis 的锁不同之处在于Lock 成功之前会一直阻塞,这与单机场景中的mutex.Lock很相似
package main
import (
    "github.com/go-zookeeper/zk"
)
func main() {
    c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second)
    if err != nil {
        panic(err)
    }
    l := zk.NewLock(c, "/lock", zk.WorldACL(zk.PermAll))
    err = l.Lock()
    if err != nil {
        panic(err)
    }
    println("lock success, do your business logic")
    time.Sleep(time.Second * 10) // 模拟业务处理
    l.Unlock()
    println("unlock success, finish business logic")
}
其原理也是基于临时Sequence节点和watch API,例如我们这里使用的是/lock节点。Lock会在该节点下的节点列表中插入自己的值,只要节点下的子节点发生变化,就会通知所有 watch 该节点的程序。这时候程序会检查当前节点下最小的子节点的 id 是否与自己的一致。如果一致,说明加锁成功了。
这种分布式的阻塞锁比较适合分布式任务调度场景,但不适合高频次持锁时间短的抢锁场景。按照 Google 的 Chubby 论文里的阐述,基于强一致协议的锁适用于粗粒度的加锁操作。这里的粗粒度指锁占用时间较长。我们在使用时也应思考在自己的业务场景中使用是否合适。
基于 etcd
etcd 是与 ZooKeeper 类似的分布式组件,也能实现与 ZooKeeper 锁相似的功能:
package main
import (
    clientv3 "go.etcd.io/etcd/client/v3"
    "go.etcd.io/etcd/client/v3/concurrency"
)
func main() {
c := make(chan os.Signal)
    signal.Notify(c)
    client, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        panic(err)
    }
    defer client.Close()
    lockKey := "/lock"
    go func() {
        session, err := concurrency.NewSession(client)
        if err != nil {
            panic(err)
        }
        m := concurrency.NewMutex(session, lockKey)
        if err := m.Lock(context.Background()); err != nil {
            panic("go1 get mutex failed " + err.Error())
        }
        fmt.Printf("go1 get mutex sucess\n")
        fmt.Println(m)
        time.Sleep(time.Duration(10) * time.Second)
        m.Unlock(context.Background())
        fmt.Printf("go1 release lock\n")
    }()
    go func() {
        time.Sleep(time.Duration(2) * time.Second)
        session, err := concurrency.NewSession(client)
        if err != nil {
            panic(err)
        }
        m := concurrency.NewMutex(session, lockKey)
        if err := m.Lock(context.Background()); err != nil {
            panic("go2 get mutex failed " + err.Error())
        }
        fmt.Printf("go2 get mutex sucess\n")
        fmt.Println(m)
        time.Sleep(time.Duration(2) * time.Second)
        m.Unlock(context.Background())
        fmt.Printf("go2 release lock\n")
    }()
    <-c
}
 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号