Go结合Redis用最简单的方式实现分布式锁

目录
  • 前言
  • 单Redis实例场景
    • 加解锁示例
    • 小结
  • 多Redis实例场景
    • 加解锁示例
    • 小结
  • 总结

前言

在项目中我们经常有需要使用分布式锁的场景,而Redis是实现分布式锁最常见的一种方式,并且我们也都希望能够把代码写得简单一点,所以今天我们尽量用最简单的方式来实现。

下面的代码使用go-redis客户端和gofakeit,参考和引用了Redis官方文章

单Redis实例场景

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

  • resource_name表示要锁定的资源
  • NX表示如果不存在则设置
  • 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的goroutine、channel、context、sync.WaitGroup等功能可以很容易的实现RedLock(30多行代码)
可以把加解锁操作封装成函数,这样就不会在业务代码里参杂太多加解锁的逻辑

到此这篇关于Go+Redis用最简单的方式实现分布式锁的文章就介绍到这了,更多相关Go Redis分布式锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • redis分布式锁的go-redis实现方法详解

    在分布式的业务中 , 如果有的共享资源需要安全的被访问和处理 , 那就需要分布式锁 分布式锁的几个原则; 1.「锁的互斥性」:在分布式集群应用中,共享资源的锁在同一时间只能被一个对象获取. 2. 「可重入」:为了避免死锁,这把锁是可以重入的,并且可以设置超时. 3. 「高效的加锁和解锁」:能够高效的加锁和解锁,获取锁和释放锁的性能也好. 4. 「阻塞.公平」:可以根据业务的需要,考虑是使用阻塞.还是非阻塞,公平还是非公平的锁. redis实现分布式锁主要靠setnx命令 1. 当key存在时失败

  • Go 语言下基于Redis分布式锁的实现方式

    分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 项目地址: https://github.com/Spongecaptain/redisLock 1. Go 原生的互斥锁 Go 原生的互斥锁即 sync 包下的 M

  • 用Go+Redis实现分布式锁的示例代码

    目录 为什么需要分布式锁 分布式锁需要具备特性 实现 Redis 锁应先掌握哪些知识点 set 命令 Redis.lua 脚本 go-zero 分布式锁 RedisLock 源码分析 关于分布式锁还有哪些实现方案 项目地址 为什么需要分布式锁 用户下单 锁住 uid,防止重复下单. 库存扣减 锁住库存,防止超卖. 余额扣减 锁住账户,防止并发操作. 分布式系统中共享同一个资源时往往需要分布式锁来保证变更资源一致性. 分布式锁需要具备特性 排他性 锁的基本特性,并且只能被第一个持有者持有. 防死锁

  • Go结合Redis用最简单的方式实现分布式锁

    目录 前言 单Redis实例场景 加解锁示例 小结 多Redis实例场景 加解锁示例 小结 总结 前言 在项目中我们经常有需要使用分布式锁的场景,而Redis是实现分布式锁最常见的一种方式,并且我们也都希望能够把代码写得简单一点,所以今天我们尽量用最简单的方式来实现. 下面的代码使用go-redis客户端和gofakeit,参考和引用了Redis官方文章 单Redis实例场景 如果熟悉Redis的命令,可能会马上想到使用Redis的set if not exists操作来实现,并且现在标准的实现

  • Redisson实现Redis分布式锁的几种方式

    目录 Redis几种架构 普通分布式锁 单机模式 哨兵模式 集群模式 总结 Redlock分布式锁 实现原理 问题合集 前几天发的一篇文章<Redlock:Redis分布式锁最牛逼的实现>,引起了一些同学的讨论,也有一些同学提出了一些疑问,这是好事儿.本文在讲解如何使用Redisson实现Redis普通分布式锁,以及Redlock算法分布式锁的几种方式的同时,也附带解答这些同学的一些疑问. Redis几种架构 Redis发展到现在,几种常见的部署架构有: 单机模式: 主从模式: 哨兵模式: 集

  • Redis分布式锁实现方式及超时问题解决

    一 前言 redis在分布式应用十分广泛,本篇文章也是互联网面试的重点内容,读者至少需要知道为什么需要分布式锁,分布式锁的实现原理,分布式锁的应用场景,在使用分布式锁时遇到哪些问题?你是如何解决的,如果读者能掌握以上问题,那么关于这道面试题,你也就基本过关了: 二 分布式锁的产生背景 分布式锁对应的是多个应用,每个应用中都可能会处理相同的数据,如果多个应用对用一个操作进行了重复操作,就会出现数据不一致,数据重复问题,于是分布式锁应用而生,通常你可以理解为多线程中的synchronized 三 分

  • 如何使用注解方式实现 Redis 分布式锁

    目录 引入 Redisson 初始化 Redisson 编写 Redisson 分布式锁工具类 声明注解 @Lock 注解解析类 引入 Redisson <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.14.1</version> </depend

  • 详细解读分布式锁原理及三种实现方式

    目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CAP理论告诉我们"任何一个分布式系统都无法同时满足一致性(Consistency).可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项."所以,很多系统在设计之初就要对这三者做出取舍.在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证"最终一致性",只要这个最终

  • redis分布式锁及会出现的问题解决

    一.redis实现分布式锁的主要原理: 1.加锁 最简单的方法是使用setnx命令.key是锁的唯一标识,按业务来决定命名.比如想要给一种商品的秒杀活动加锁,可以给key命名为 "lock_sale_商品ID" .而value设置成什么呢?我们可以姑且设置成1.加锁的伪代码如下: setnx(key,1) 当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁:当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败. 2.解锁 有加锁就得有解锁.当得到锁的

  • Redis分布式锁升级版RedLock及SpringBoot实现方法

    分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式.但是现在公司都是流行分布式架构,在分布式环境下,如何保证不同节点的线程同步执行呢?因此就引出了分布式锁,它是控制分布式系统之间互斥访问共享资源的一种方式. 在一个分布式系统中,多台机器上部署了多个服务,当客户端一个用户发起一个数据插入请求时,如果没有分布式锁机制保证,那么那多台机器上的多个服务可能进行并发插

  • 如何操作Redis和zookeeper实现分布式锁

    如何操作Redis和zookeeper实现分布式锁 在分布式场景下,有很多种情况都需要实现最终一致性.在设计远程上下文的领域事件的时候,为了保证最终一致性,在通过领域事件进行通讯的方式中,可以共享存储(领域模型和消息的持久化数据源),或者做全局XA事务(两阶段提交,数据源可分开),也可以借助消息中间件(消费者处理需要能幂等).通过Observer模式来发布领域事件可以提供很好的高并发性能,并且事件存储也能追溯更小粒度的事件数据,使各个应用系统拥有更好的自治性. 1.分布式锁 分布式锁一般用在分布

  • SpringBoot使用Redis实现分布式锁

    前言 在单机应用时代,我们对一个共享的对象进行多线程访问的时候,使用java的synchronized关键字或者ReentrantLock类对操作的对象加锁就可以解决对象的线程安全问题. 分布式应用时代这个方法却行不通了,我们的应用可能被部署到多台机器上,运行在不同的JVM里,一个对象可能同时存在多台机器的内存中,怎样使共享对象同时只被一个线程处理就成了一个问题. 在分布式系统中为了保证一个对象在高并发的情况下只能被一个线程使用,我们需要一种跨JVM的互斥机制来控制共享资源的访问,此时就需要用到

  • 自定义注解+Spel实现分布式锁方式

    目录 自定义注解+Spel实现分布式锁 依赖 RedisLockRegistryConfig 自定义注解 自定义切面 测试类 执行结果 基于注解的方式实现分布式锁 redis分布式锁的实现 测试 自定义注解+Spel实现分布式锁 依赖 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xs

随机推荐