文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-06-28 06:01

关注

这篇文章主要介绍了Go结合Redis怎么实现分布式锁,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。

单Redis实例场景

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

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

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

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

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

加解锁示例

package mainimport (   "context"   "errors"   "fmt"   "github.com/brianvoe/gofakeit/v6"   "github.com/go-redis/redis/v8"   "sync"   "time")var client *redis.Clientconst unlockScript = `if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end`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()函数,其中有一个操作会因为拿不到锁而直接失败。

小结

多Redis实例场景

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

加解锁示例

package mainimport (   "context"   "errors"   "fmt"   "github.com/brianvoe/gofakeit/v6"   "github.com/go-redis/redis/v8"   "sync"   "time")var clients []*redis.Clientconst unlockScript = `if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end`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释放锁逻辑,释放锁逻辑很简单,只是把成功拿到的锁给释放掉即可。
最后判断成功获取到的锁的数量是否大于一半,如果没有得到一半以上的锁,说明加锁失败。
如果加锁成功接下来就是进行业务处理。

小结

感谢你能够认真阅读完这篇文章,希望小编分享的“Go结合Redis怎么实现分布式锁”这篇文章对大家有帮助,同时也希望大家多多支持编程网,关注编程网行业资讯频道,更多相关知识等着你来学习!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯