Go与Redis实现分布式互斥锁和红锁

目录
  • 前言
  • 互斥锁
  • TryLock和Unlock实现
    • Lock实现
  • 实现看门狗机制
    • 看门狗实现
  • 红锁
    • 加锁实现
    • 看门狗实现
    • 解锁实现

前言

在项目中我们经常有需要使用分布式锁的场景,而Redis是实现分布式锁最常见的一种方式,这篇文章主要是使用Go+Redis实现互斥锁和红锁。

下面的代码使用go-redis客户端和gofakeit库。

代码地址

互斥锁

Redis里有一个设置如果不存在的命令,我们可以通过这个命令来实现互斥锁功能,在Redis官方文档里面推荐的标准实现方式是SET resource_name my_random_value NX PX 30000这串命令,其中:

  • resource_name表示要锁定的资源
  • NX表示如果不存在则设置
  • PX 30000表示过期时间为30000毫秒,也就是30秒
  • my_random_value这个值在所有的客户端必须是唯一的,所有同一key的锁竞争者这个值都不能一样。

值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功,避免错误释放别的竞争者的锁。

由于涉及到两个操作,因此我们需要通过Lua脚本保证操作的原子性:

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

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

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

TryLock和Unlock实现

TryLock其实就是使用SET resource_name my_random_value NX PX 30000加锁,这里使用UUID作为随机值,并且在加锁成功时把随机值返回,这个随机值会在Unlock时使用;

Unlock解锁逻辑就是执行前面说到的lua脚本

func (l *Lock) TryLock(ctx context.Context) error {
   success, err := l.client.SetNX(ctx, l.resource, l.randomValue, ttl).Result()
   if err != nil {
      return err
   }
   // 加锁失败
   if !success {
      return ErrLockFailed
   }
   // 加锁成功
   l.randomValue = randomValue
   return nil
}

func (l *Lock) Unlock(ctx context.Context) error {
   return l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err()
}

Lock实现

Lock是阻塞的获取锁,因此在加锁失败的时候,需要重试。当然也可能出现其他异常情况(比如网络问题,请求超时等),这些情况则直接返回error

步骤如下:

  • 尝试加锁,加锁成功直接返回
  • 加锁失败则不断循环尝试加锁直到成功或出现异常情况
func (l *Lock) Lock(ctx context.Context) error {
	// 尝试加锁
	err := l.TryLock(ctx)
	if err == nil {
		return nil
	}
	if !errors.Is(err, ErrLockFailed) {
		return err
	}
	// 加锁失败,不断尝试
	ticker := time.NewTicker(l.tryLockInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			// 超时
			return ErrTimeout
		case <-ticker.C:
			// 重新尝试加锁
			err := l.TryLock(ctx)
			if err == nil {
				return nil
			}
			if !errors.Is(err, ErrLockFailed) {
				return err
			}
		}
	}
}

实现看门狗机制

我们前面的例子中提到的互斥锁有一个小问题,就是如果持有锁客户端A被阻塞,那么A的锁可能会超时被自动释放,导致客户端B提前获取到锁。

为了减少这种情况的发生,我们可以在A持有锁期间,不断地延长锁的过期时间,减少客户端B提前获取到锁的情况,这就是看门狗机制。

当然,这没办法完全避免上述情况的发生,因为如果客户端A获取锁之后,刚好与Redis的连接关闭了,这时候也就没办法延长超时时间了。

看门狗实现

加锁成功时启动一个线程,不断地延长锁地过期时间;在Unlock时关闭看门狗线程。

看门狗流程如下:

  • 加锁成功,启动看门狗
  • 看门狗线程不断延长锁的过程时间
  • 解锁,关闭看门狗
func (l *Lock) startWatchDog() {
	ticker := time.NewTicker(l.ttl / 3)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			// 延长锁的过期时间
			ctx, cancel := context.WithTimeout(context.Background(), l.ttl/3*2)
			ok, err := l.client.Expire(ctx, l.resource, l.ttl).Result()
			cancel()
			// 异常或锁已经不存在则不再续期
			if err != nil || !ok {
				return
			}
		case <-l.watchDog:
			// 已经解锁
			return
		}
	}
}

TryLock:启动看门狗

func (l *Lock) TryLock(ctx context.Context) error {
	success, err := l.client.SetNX(ctx, l.resource, l.randomValue, l.ttl).Result()
	if err != nil {
		return err
	}
	// 加锁失败
	if !success {
		return ErrLockFailed
	}
	// 加锁成功,启动看门狗
	go l.startWatchDog()
	return nil
}

Unlock:关闭看门狗

func (l *Lock) Unlock(ctx context.Context) error {
	err := l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err()
	// 关闭看门狗
	close(l.watchDog)
	return err
}

红锁

由于上面的实现是基于单Redis实例,如果这个唯一的实例挂了,那么所有请求都会因为拿不到锁而失败,为了提高容错性,我们可以使用多个分布在不同机器上的Redis实例,并且只要拿到其中大多数节点的锁就能加锁成功,这就是红锁算法。它其实也是基于上面的单实例算法的,只是我们需要同时对多个Redis实例获取锁。

加锁实现

在加锁逻辑里,我们主要是对每个Redis实例执行SET resource_name my_random_value NX PX 30000获取锁,然后把成功获取锁的客户端放到一个channel里(这里因为是多线程并发获取锁,使用slice可能有并发问题),同时使用sync.WaitGroup等待所有获取锁操作结束。

然后判断成功获取到的锁的数量是否大于一半,如果没有得到一半以上的锁,说明加锁失败,释放已经获得的锁。

如果加锁成功,则启动看门狗延长锁的过期时间。

func (l *RedLock) TryLock(ctx context.Context) error {
	randomValue := gofakeit.UUID()
	var wg sync.WaitGroup
	wg.Add(len(l.clients))
	// 成功获得锁的Redis实例的客户端
	successClients := make(chan *redis.Client, len(l.clients))
	for _, client := range l.clients {
		go func(client *redis.Client) {
			defer wg.Done()
			success, err := client.SetNX(ctx, l.resource, randomValue, ttl).Result()
			if err != nil {
				return
			}
			// 加锁失败
			if !success {
				return
			}
			// 加锁成功,启动看门狗
			go l.startWatchDog()
			successClients <- client
		}(client)
	}
	// 等待所有获取锁操作完成
	wg.Wait()
	close(successClients)
	// 如果成功加锁得客户端少于客户端数量的一半+1,表示加锁失败
	if len(successClients) < len(l.clients)/2+1 {
		// 就算加锁失败,也要把已经获得的锁给释放掉
		for client := range successClients {
			go func(client *redis.Client) {
				ctx, cancel := context.WithTimeout(context.Background(), ttl)
				l.script.Run(ctx, client, []string{l.resource}, randomValue)
				cancel()
			}(client)
		}
		return ErrLockFailed
	}

	// 加锁成功,启动看门狗
	l.randomValue = randomValue
	l.successClients = nil
	for successClient := range successClients {
		l.successClients = append(l.successClients, successClient)
	}

	return nil
}

看门狗实现

我们需要延长所有成功获取到的锁的过期时间。

func (l *RedLock) startWatchDog() {
	l.watchDog = make(chan struct{})
	ticker := time.NewTicker(resetTTLInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			// 延长锁的过期时间
			for _, client := range l.successClients {
				go func(client *redis.Client) {
					ctx, cancel := context.WithTimeout(context.Background(), ttl-resetTTLInterval)
					client.Expire(ctx, l.resource, ttl)
					cancel()
				}(client)
			}
		case <-l.watchDog:
			// 已经解锁
			return
		}
	}
}

解锁实现

我们需要解锁所有成功获取到的锁。

func (l *RedLock) Unlock(ctx context.Context) error {
   for _, client := range l.successClients {
      go func(client *redis.Client) {
         l.script.Run(ctx, client, []string{l.resource}, l.randomValue)
      }(client)
   }
   // 关闭看门狗
   close(l.watchDog)
   return nil
}

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

(0)

相关推荐

  • GO语言并发编程之互斥锁、读写锁详解

    在本节,我们对Go语言所提供的与锁有关的API进行说明.这包括了互斥锁和读写锁.我们在第6章描述过互斥锁,但却没有提到过读写锁.这两种锁对于传统的并发程序来说都是非常常用和重要的. 一.互斥锁 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段.它由标准库代码包sync中的Mutex结构体类型代表.sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法--Lock和Unlock.顾名思义,前者被用于锁定当前的互斥量,而后者则被用来对当前的互斥量进行解锁. 类型sy

  • Go语言的互斥锁的详细使用

    目录 前言 Go语言互斥锁设计实现 mutex介绍 Lock加锁 初始化状态 自旋 抢锁准备期望状态 通过CAS操作更新期望状态 解锁 非阻塞加锁 总结 前言 当提到并发编程.多线程编程时,都会在第一时间想到锁,锁是并发编程中的同步原语,他可以保证多线程在访问同一片内存时不会出现竞争来保证并发安全:在Go语言中更推崇由channel通过通信的方式实现共享内存,这个设计点与许多主流编程语言不一致,但是Go语言也在sync包中提供了互斥锁.读写锁,毕竟channel也不能满足所有场景,互斥锁.读写锁

  • GO语言协程互斥锁Mutex和读写锁RWMutex用法实例详解

    sync.Mutex Go中使用sync.Mutex类型实现mutex(排他锁.互斥锁).在源代码的sync/mutex.go文件中,有如下定义: // A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { state int32 sem

  • Go与Redis实现分布式互斥锁和红锁

    目录 前言 互斥锁 TryLock和Unlock实现 Lock实现 实现看门狗机制 看门狗实现 红锁 加锁实现 看门狗实现 解锁实现 前言 在项目中我们经常有需要使用分布式锁的场景,而Redis是实现分布式锁最常见的一种方式,这篇文章主要是使用Go+Redis实现互斥锁和红锁. 下面的代码使用go-redis客户端和gofakeit库. 代码地址 互斥锁 Redis里有一个设置如果不存在的命令,我们可以通过这个命令来实现互斥锁功能,在Redis官方文档里面推荐的标准实现方式是SET resour

  • Redis分布式锁之红锁的实现

    目录 一.问题 二.办法 三.原理 四.实战 一.问题 分布式锁,当我们请求一个分布式锁的时候,成功了,但是这时候slave还没有复制我们的锁,masterDown了,我们的应用继续请求锁的时候,会从继任了master的原slave上申请,也会成功. 这就会导致,同一个锁被获取了不止一次. 二.办法 Redis中针对此种情况,引入了红锁的概念. 三.原理 用Redis中的多个master实例,来获取锁,只有大多数实例获取到了锁,才算是获取成功.具体的红锁算法分为以下五步: 获取当前的时间(单位是

  • Redis中Redisson红锁(Redlock)使用原理

    目录 简介 为什么使用Redis的红锁 解决方案:使用红锁 Redisson红锁实例 Redisson红锁原理 参考文章 简介 说明 本文介绍为什么要使用Redis的红锁(Redlock).什么是Redis的红锁以及Redis红锁的原理. 本文用Redisson来介绍Redis红锁的用法. Redisson 高版本会根据redisClient的模式来决定getLock返回的锁类型,如果集群模式,满足红锁的条件,则会直接返回红锁. 官网 REDIS distlock -- Redis中国用户组(C

  • Redis实现分布式锁的方法示例

    之前我们使用的定时任务都是只部署在了单台机器上,为了解决单点的问题,为了保证一个任务,只被一台机器执行,就需要考虑锁的问题,于是就花时间研究了这个问题.到底怎样实现一个分布式锁呢? 锁的本质就是互斥,保证任何时候能有一个客户端持有同一个锁,如果考虑使用redis来实现一个分布式锁,最简单的方案就是在实例里面创建一个键值,释放锁的时候,将键值删除.但是一个可靠完善的分布式锁需要考虑的细节比较多,我们就来看看如何写一个正确的分布式锁. 单机版分布式锁 SETNX 所以我们直接基于 redis 的 s

  • Redis实现分布式锁的几种方法总结

    Redis实现分布式锁的几种方法总结 分布式锁是控制分布式系统之间同步访问共享资源的一种方式.在分布式系统中,常常需要协调他们的动作.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁. 我们来假设一个最简单的秒杀场景:数据库里有一张表,column分别是商品ID,和商品ID对应的库存量,秒杀成功就将此商品库存量-1.现在假设有1000个线程来秒杀两件商品,500个线程秒杀第一个商品,

  • SpringBoot使用Redis实现分布式锁

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

  • 基于redis实现分布式锁的原理与方法

    前言 系统的不断扩大,分布式锁是最基本的保障.与单机的多线程不一样的是,分布式跨多个机器.线程的共享变量无法跨机器. 为了保证一个在高并发存场景下只能被同一个线程操作,java并发处理提供ReentrantLock或Synchronized进行互斥控制.但是这仅仅对单机环境有效.我们实现分布式锁大概通过三种方式. redis实现分布式锁 数据库实现分布式锁 zk实现分布式锁 今天我们介绍通过redis实现分布式锁.实际上这三种和java对比看属于一类.都是属于程序外部锁. 原理剖析 上述三种分布

  • SpringBoot之使用Redis实现分布式锁(秒杀系统)

    一.Redis分布式锁概念篇 建议直接采用Redis的官方推荐的Redisson作为redis的分布式锁 1.1.为什么要使用分布式锁 我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行,毫无Bug! 注意这是单机应用,也就是所有的请求都会分配到当前服务器的JVM内部,然后映射为操作系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间! 后来业务发展,需要做集群,一个应用需要部署到几台机

  • 基于Redis实现分布式锁的方法(lua脚本版)

    1.前言 在Java中,我们通过锁来避免由于竞争而造成的数据不一致问题.通常我们使用synchronized .Lock来实现.但是Java中的锁只能保证在同一个JVM进程内中可用,在跨JVM进程,例如分布式系统上则不可靠了. 2.分布式锁 分布式锁,是一种思想,它的实现方式有很多,如基于数据库实现.基于缓存(Redis等)实现.基于Zookeeper实现等等.为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件 互斥性:在任意时刻,只有一个客户端能持有锁. 不会发生死锁:即使客户端

  • Java基于redis实现分布式锁

    为了保证一个在高并发存场景下只能被同一个线程操作,java并发处理提供ReentrantLock或Synchronized进行互斥控制.但是这仅仅对单机环境有效.我们实现分布式锁大概通过三种方式. redis实现分布式锁 数据库实现分布式锁 zk实现分布式锁 实际上这三种和java对比看属于一类.都是属于程序外部锁. 原理剖析 上述三种分布式锁都是通过各自为依据对各个请求进行上锁,解锁从而控制放行还是拒绝.redis锁是基于其提供的setnx命令. setnx当且仅当key不存在.若给定key已

随机推荐