分布式锁

2021-11-07  本文已影响0人  wayyyy

redis 实现分布式锁

环境准备
redis 是单实例

实现分布式锁需要的很早期命令SETNX,这个命令表示 SET If Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。

两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。
启动一个redis-cli1

# redis-cli -h 127.0.0.1 -p 6679 
127.0.0.1:6679> SETNX lock 1
(integer) 1

再启动另一个redis-cli2:

redis-cli -h 127.0.0.1 -p 6679
127.0.0.1:6679> SETNX lock 1
(integer) 0
127.0.0.1:6679> get lock 
"1"

redis-cli1 使用del 释放锁

redis-cli2 获取锁:

上面的实现已经基本实现了互斥的功能,但还不够,试想:假设当客户端1拿到锁后,进程挂了,那么就会导致这个客户端一直占用这个锁,其他客户端也就永远拿不到这个锁了。

那想说,这样也好办,再使用命令给这个锁设置一个过期时间:

127.0.0.1:6679> setex lock 10 1
OK
127.0.0.1:6679> ttl lock
(integer) 6

这样,无论客户端是否异常,这个锁都可以在一定时间后被自动释放,其它客户端依旧可以拿到锁。

但就是如此,还是会遇到2个问题:

Demo:

package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "sync"
    "time"
)

var lockKey = "counter_lock"
var counterKey = "counter"

func incr(grNum int) {
    client := redis.NewClient(&redis.Options{
        Addr:     "192.168.48.139:6679",
        Password: "",
        DB:       0,
    })

    // lock
    resp := client.SetNX(lockKey, 1, time.Second*5)
    lockSuccess, err := resp.Result()
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(grNum, "lock result: ", lockSuccess)
    if !lockSuccess {
        return
    }
    
    // counter++
    getResp := client.Get(counterKey)
    cntValue, err := getResp.Int64()
    if err == nil {
        cntValue++
        resp := client.Set(counterKey, cntValue, 0)
        _, err := resp.Result()
        if err != nil {
            fmt.Println(grNum, "set value error", err.Error())
        }
    } else {
        fmt.Println(grNum, "get value error", err.Error())
    }

    fmt.Println(grNum, "current counter is ", cntValue)

    // unlock
    delResp := client.Del(lockKey)
    unlockSuccess, err := delResp.Result()
    if err == nil && unlockSuccess > 0 {
        fmt.Println(grNum, "unlock success")
    } else {
        fmt.Println(grNum, "unlock failed", err)
    }
}

func main() {
    client := redis.NewClient(&redis.Options{
        Addr:     "192.168.48.139:6679",
        Password: "",
        DB:       0,
    })

    resp := client.Set(counterKey, 0, 0)
    _, err := resp.Result()
    if err != nil {
        fmt.Println("set value error", err.Error())
        return
    }

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        time.Sleep(time.Microsecond * 100)
        wg.Add(1)
        go func(grNum int) {
            defer wg.Done()
            incr(grNum)
        }(i)
    }
    wg.Wait()
}

输出:


image.png
redis 是多实例

上面分析的场景都是,锁在单个Redis 实例中可能产生的问题,而实际在使用 Redis 时,一般会采用 主从集群 + 哨兵的模式部署,用来保证可用性。

那当主从发生切换时,这个分布式锁会依旧安全吗?
想想这样的场景:客户端 1 在主库上执行 SET 命令,加锁成功,此时,主库异常宕机,SET 命令还未同步到从库上(主从复制是异步的)从库被哨兵提升为新主库,这时锁在新的主库上就丢失了。

为了解决这一问题
TODO

zookeeper 实现分布式锁

TODO

zookeeper 的锁基于Redis 锁不同之处在于Lock成功之前会一直阻塞,这与单价的 mutex.Lock 行为相似。
这种分布式阻塞锁适合分布式任务调度场景,但不适合高频次持锁时间段的抢锁场景。

etcd 实现分布式锁

TODO

总结:如何挑选合适的分布式锁?

在业务规模不大,QPS很小的情况下,使用哪种分布式锁都差不多,如果公司内已有可以使用的zookeeper,etcd 或者 redis 集群,那就尽量在不引入新技术栈的情况下满足业务需求。


参考资料
1、

上一篇 下一篇

猜你喜欢

热点阅读