Go结合Redis怎么实现分布式锁

    单Redis实例场景

    如果熟悉Redis的命令,可能会马上想到使用Redis的set if not exists操作来实现,并且现在标准的实现方式是SET resource_name my_random_value NX PX 30000这串命令,其中:

    • resource_name表示要锁定的资源

    • NX表示如果不存在则设置

    • Go+Redis!如何实现分布式锁

      PX 30000表示过期时间为30000毫秒,也就是30秒

    • my_random_value这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样。

    value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下Lua脚本实现:

    if redis.call("
    get"
    ,KEYS[1]) == ARGV[1] then
    return redis.call("
    del"
    ,KEYS[1])
    else
    return 0
    end

    举个例子:客户端A取得资源锁,但是紧接着被一个其他操作阻塞了,当客户端A运行完毕其他操作后要释放锁时,原来的锁早已超时并且被Redis自动释放,并且在这期间资源锁又被客户端B再次获取到。

    使用Lua脚本是因为判断和删除是两个操作,所以有可能A刚判断完锁就过期自动释放了,然后B就获取到了锁,然后A又调用了Del,导致把B的锁给释放了。

    加解锁示例package main

    import (
    "
    context"

    "
    errors"

    "
    fmt"

    "
    github.com/brianvoe/gofakeit/v6"

    "
    github.com/go-redis/redis/v8"

    "
    sync"

    "
    time"

    )

    var client *redis.Client

    const unlockScript = `
    if redis.call("
    get"
    ,KEYS[1]) == ARGV[1] then
    return redis.call("
    del"
    ,KEYS[1])
    else
    return 0
    end`

    func lottery(ctx context.Context) error {
    // 加锁
    myRandomValue := gofakeit.UUID()
    resourceName := "
    resource_name"

    ok, err := client.SetNX(ctx, resourceName, myRandomValue, time.Second*30).Result()
    if err != nil {
    return err
    }
    if !ok {
    return errors.New("
    系统繁忙,请重试"
    )
    }
    // 解锁
    defer func() {
    script := redis.NewScript(unlockScript)
    script.Run(ctx, client, []string{resourceName}, myRandomValue)
    }()

    // 业务处理
    time.Sleep(time.Second)
    return nil
    }

    func main() {
    client = redis.NewClient(&
    redis.Options{
    Addr: "
    127.0.0.1:6379"
    ,
    })
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
    defer wg.Done()
    ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
    err := lottery(ctx)
    if err != nil {
    fmt.Println(err)
    }
    }()
    go func() {
    defer wg.Done()
    ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
    err := lottery(ctx)
    if err != nil {
    fmt.Println(err)
    }
    }()
    wg.Wait()
    }

    我们先看lottery()函数,这里模拟一个抽奖操作,在进入函数时,先使用SET resource_name my_random_value NX PX 30000加锁,这里使用UUID作为随机值,如果操作失败,直接返回,让用户重试,如果成功在defer里面执行解锁逻辑,解锁逻辑就是执行前面说到得lua脚本,然后再进行业务处理。

    我们在main()函数里面执行了两个goroutine并发调用lottery()函数,其中有一个操作会因为拿不到锁而直接失败。

    小结
    • 生成随机值

    • 使用SET resource_name my_random_value NX PX 30000加锁

    • 如果加锁失败,直接返回

    • defer添加解锁逻辑,保证在函数退出的时候会执行

    • 执行业务逻辑

    多Redis实例场景

    在单实例情况下,如果这个实例挂了,那么所有请求都会因为拿不到锁而失败,所以我们需要多个分布在不同机器上的Redis实例,并且拿到其中大多数节点的锁才能加锁成功,这也就是RedLock算法。我们需要同时对多个Redis实例获取锁,但它实际上也是基于单实例算法的。

    加解锁示例package main

    import (
    "
    context"

    "
    errors"

    "
    fmt"

    "
    github.com/brianvoe/gofakeit/v6"

    "
    github.com/go-redis/redis/v8"

    "
    sync"

    "
    time"

    )

    var clients []*redis.Client

    const unlockScript = `
    if redis.call("
    get"
    ,KEYS[1]) == ARGV[1] then
    return redis.call("
    del"
    ,KEYS[1])
    else
    return 0
    end`

    func lottery(ctx context.Context) error {
    // 加锁
    myRandomValue := gofakeit.UUID()
    resourceName := "
    resource_name"

    var wg sync.WaitGroup
    wg.Add(len(clients))
    // 这里主要是确保不要加锁太久,这样会导致业务处理的时间变少
    lockCtx, _ := context.WithTimeout(ctx, time.Millisecond*5)
    // 成功获得锁的Redis实例的客户端
    successClients := make(chan *redis.Client, len(clients))
    for _, client := range clients {
    go func(client *redis.Client) {
    defer wg.Done()
    ok, err := client.SetNX(lockCtx, resourceName, myRandomValue, time.Second*30).Result()
    if err != nil {
    return
    }
    if !ok {
    return
    }
    successClients <
    - client
    }(client)
    }
    wg.Wait() // 等待所有获取锁操作完成
    close(successClients)
    // 解锁,不管加锁是否成功,最后都要把已经获得的锁给释放掉
    defer func() {
    script := redis.NewScript(unlockScript)
    for client := range successClients {
    go func(client *redis.Client) {
    script.Run(ctx, client, []string{resourceName}, myRandomValue)
    }(client)
    }
    }()
    // 如果成功加锁得客户端少于客户端数量的一半+1,表示加锁失败
    if len(successClients) <
    len(clients)/2+1 {
    return errors.New("
    系统繁忙,请重试"
    )
    }

    // 业务处理
    time.Sleep(time.Second)
    return nil
    }

    func main() {
    clients = append(clients, redis.NewClient(&
    redis.Options{
    Addr: "
    127.0.0.1:6379"
    ,
    DB: 0,
    }), redis.NewClient(&
    redis.Options{
    Addr: "
    127.0.0.1:6379"
    ,
    DB: 1,
    }), redis.NewClient(&
    redis.Options{
    Addr: "
    127.0.0.1:6379"
    ,
    DB: 2,
    }), redis.NewClient(&
    redis.Options{
    Addr: "
    127.0.0.1:6379"
    ,
    DB: 3,
    }), redis.NewClient(&
    redis.Options{
    Addr: "
    127.0.0.1:6379"
    ,
    DB: 4,
    }))
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
    defer wg.Done()
    ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
    err := lottery(ctx)
    if err != nil {
    fmt.Println(err)
    }
    }()
    go func() {
    defer wg.Done()
    ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
    err := lottery(ctx)
    if err != nil {
    fmt.Println(err)
    }
    }()
    wg.Wait()
    time.Sleep(time.Second)
    }

    在上面的代码中,我们使用Redis的多数据库模拟多个Redis master实例,一般我们会选择5个Redis实例,真实环境中这些实例应该是分布在不同机器上的,避免同时失效。在加锁逻辑里,我们主要是对每个Redis实例执行SET resource_name my_random_value NX PX 30000获取锁,然后把成功获取锁的客户端放到一个channel里(这里使用slice可能有并发问题),同时使用sync.WaitGroup等待所以获取锁操作结束。然后添加defer释放锁逻辑,释放锁逻辑很简单,只是把成功拿到的锁给释放掉即可。最后判断成功获取到的锁的数量是否大于一半,如果没有得到一半以上的锁,说明加锁失败。如果加锁成功接下来就是进行业务处理。

    小结
    • 生成随机值

    • 并发给每个Redis实例使用SET resource_name my_random_value NX PX 30000加锁

    • 等待所有获取锁操作完成

    • defer添加解锁逻辑,保证在函数退出的时候会执行,这里先defer再判断是因为有可能获取到一部分Redis实例的锁,但是因为没有超过一半,还是会判断为加锁失败

    • 判断是否拿到一半以上Redis实例的锁,如果没有说明加锁失败,直接返回

    • 执行业务逻辑



    引言:Go和Redis的完美结合带来了无限可能
    Go语言以其高效、轻量级的特性深受程序员喜爱,而Redis则以其快速、高可靠性的特点成为流行的键值存储数据库。二者的完美结合可以为分布式锁的实现带来更便利的条件。本文将介绍在Go语言中如何结合Redis实现分布式锁的详细步骤。
    第一步:先基于Redis实现一个互斥锁
    在Go语言中实现分布式锁,首先需要基于Redis实现一个互斥锁。互斥锁遵循先来先得的规则,当多个进程在Redis中操纵同一套锁的时候,只会让其中一个获得锁,并同意其它进程等待,直到获得锁的进程释放掉锁的时候再轮到其它进程再次争夺锁。
    第二步:将互斥锁用Go语言进行封装
    Go语言可以将Redis中的锁进行封装,帮助我们在代码实现中使用更加方便。对单节点的锁进行封装有很多现成的库可以使用,比如Redsync-go、redislock-go。而涉及到多节点的分布式锁,则需要进行进一步的封装。
    第三步:实现简单的分布式锁
    通过以上两个步骤,我们已经能够基于Go和Redis实现一个基础的互斥锁。接下来,我们将根据业务需求,实现一个简单的分布式锁。该实现将有以下几个步骤:
    1. 从Redis中获得锁;
    2. 设置锁的过期时间,防止死锁;
    3. 在使用锁的业务逻辑完成后,释放锁。
    第四步:完善分布式锁协议
    上述实现是最简单的分布式锁,但是这种方式存在一些问题。例如,在一个节点上获取锁后,可能并不会释放锁。这就需要采用更加健壮的锁协议。
    常用的锁协议有Redlock、Zookeeper、etcd等等。Redlock协议是目前被广泛使用的分布式锁协议,能够在大多数情况下保证正确性。通过使用Redlock协议,我们可以保证锁在分布式系统中的可靠性。
    第五步:研究Redlock协议的实现
    Redlock协议实现的核心思想是在互相独立的Redis节点上对指定的资源进行加锁。例如,先确定一个密钥,然后在各个Redis节点上同时创建一份锁,然后通过集群的大多数节点的确认获得锁,并释放锁。
    Redis的集群架构最少也要有三个节点,获得锁的节点数量要大于节点总数的一半。该协议还考虑了节点的时间同步对锁的安全性的影响,确保节点间时间的同步性。
    第六步:使用Redlock协议完成分布式锁
    可以使用一些现成的Redlock协议的Go库去实现最终的分布式锁。这个过程中,需要根据该协议的算法,将业务代码中的锁实现进行替换即可实现分布式锁。
    第七步:注意事项
    使用分布式锁的过程中,有一些要注意的地方:
    1. 不能在高并发情况下使用setTimeout()函数延长锁的过期时间,否则会设法窃取锁;
    2. 获得分布式锁的时间应该越短越好;
    3. 对锁进行监控,确保它始终处于锁定状态,并设置主动释放锁的机制防止锁一直被占用;
    4. 避免出现“惊群效应”,即同时请求锁节点的负载过高导致节点崩溃。
    总结:Go语言和Redis的完美结合使得分布式锁的实现更加便利。在实现过程中,需要深入了解锁协议,并在代码级别进行锁的实现。同时,还需要注意常见的分布式锁问题,确保系统的健壮性和性能表现。