Golang实现可重入锁的示例代码


Posted in Golang onMay 25, 2022

项目中遇到了可重入锁的需求和实现,具体记录下。

什么是可重入锁

我们平时说的分布式锁,一般指的是在不同服务器上的多个线程中,只有一个线程能抢到一个锁,从而执行一个任务。而我们使用锁就是保证一个任务只能由一个线程来完成。所以我们一般是使用这样的三段式逻辑:

Lock();
DoJob();
Unlock();

但是由于我们的系统都是分布式的,这个锁一般不会只放在某个进程中,我们会借用第三方存储,比如 Redis 来做这种分布式锁。但是一旦借助了第三方存储,我们就必须面对这个问题:Unlock是否能保证一定运行呢?

这个问题,我们面对的除了程序的bug之外,还有网络的不稳定,进程被杀死,服务器被down机等。我们是无法保证Unlock一定被运行的。

那么我们就一般在Lock的时候为这个锁加一个超时时间作为兜底。

LockByExpire(duration);
DoJob();
Unlock();

这个超时时间是为了一旦出现异常情况导致Unlock没有被运行,这个锁在duration时间内也会被自动释放。这个在redis中我们一般就是使用set ex 来进行锁超时的设定。

但是有这个超时时间我们又遇上了问题,超时时间设置多久合适呢?当然要设置的比 DoJob 消耗的时间更长,否则的话,在任务还没结束的时候,锁就被释放了,还是有可能导致并发任务的存在。

但是实际上,同样由于网络超时问题,系统运行状况问题等,我们是无法准确知道DoJob这个函数要执行多久的。那么这时候怎么办呢?

有两个办法:

第一个方法,我们可以对DoJob做一个超时设置。让DoJob最多只能执行n秒,那么我的分布式锁的超时时长设置比n秒长就可以了。为一个任务设置超时时间在很多语言是可以做到的。比如golang 中的 TimeoutContext。

而第二种方法,就是我们先为锁设置一个比较小的超时时长,然后不断续期这个锁。对一个锁的不断需求,也可以理解为重新开始加锁,这种可以不断续期的锁,就叫做可重入锁。

除了主线程之外,可重入锁必然有一个另外的线程(或者携程)可以对这个锁进行续期,我们叫这个额外的程序叫做watchDog(看门狗)。

具体实现

在Golang中,语言级别天生支持协程,所以这种可重入锁就非常容易实现:

// DistributeLockRedis 基于redis的分布式可重入锁,自动续租
type DistributeLockRedis struct {
	key       string             // 锁的key
	expire    int64              // 锁超时时间
	status    bool               // 上锁成功标识
	cancelFun context.CancelFunc // 用于取消自动续租携程
	redis     redis.Client       // redis句柄
}

// 创建可
func NewDistributeLockRedis(key string, expire int64) *DistributeLockRedis {
	return &DistributeLockRedis{
		 key : key,
		 expire : expire,
	}
}

// TryLock 上锁
func (dl *DistributeLockRedis) TryLock() (err error) {
	if err = dl.lock(); err != nil {
		return err
	}
	ctx, cancelFun := context.WithCancel(context.Background())
	dl.cancelFun = cancelFun
	dl.startWatchDog(ctx) // 创建守护协程,自动对锁进行续期
	dl.status = true
	return nil
}

// competition 竞争锁
func (dl *DistributeLockRedis) lock() error {
	if res, err := redis.String(dl.redis.Do(context.Background(), "SET", dl.key, 1, "NX", "EX", dl.expire)); err != nil {
		return err
	} 
	return nil
}


// guard 创建守护协程,自动续期
func (dl *DistributeLockRedis) startWatchDog(ctx context.Context) {
	safeGo(func() error {
		for {
			select {
			// Unlock通知结束
			case <-ctx.Done():
				return nil
			default:
				// 否则只要开始了,就自动重入(续租锁)
				if dl.status {
					if res, err := redis.Int(dl.redis.Do(context.Background(), "EXPIRE", dl.key, dl.expire)); err != nil {
						return nil
					} 
					// 续租时间为 expire/2 秒
					time.Sleep(time.Duration(dl.expire/2) * time.Second)
				}
			}
		}
	})
}

// Unlock 释放锁
func (dl *DistributeLockRedis) Unlock() (err error) {
	// 这个重入锁必须取消,放在第一个地方执行
	if dl.cancelFun != nil {
		dl.cancelFun() // 释放成功,取消重入锁
	}
	var res int
	if dl.status {
		if res, err = redis.Int(dl.redis.Do(context.Background(), "Del", dl.key)); err != nil {
			return fmt.Errorf("释放锁失败")
		}
		if res == 1 {
			dl.status = false
			return nil
		}
	}
	return fmt.Errorf("释放锁失败")
}

这段代码的逻辑基本上都以注释的形式来写了。其中主要就在startWatchDog,对锁进行重新续期

ctx, cancelFun := context.WithCancel(context.Background())
dl.cancelFun = cancelFun
dl.startWatchDog(ctx) // 创建守护协程,自动对锁进行续期
dl.status = true

首先创建一个cancelContext,它的context函数cancelFunc是给Unlock进行调用的。然后启动一个goroutine进程来循环续期。

这个新启动的goroutine在主goroutine处理结束,调用Unlock的时候,才会结束,否则会在 过期时间/2 的时候,调用一次redis的expire命令来进行续期。

至于外部,在使用的时候如下

func Foo() error {
  key := foo
  
  // 创建可重入的分布式锁
	dl := NewDistributeLockRedis(key, 10)
	// 争抢锁
	err := dl.TryLock()
	if err != nil {
		// 没有抢到锁
		return err
	}
	
	// 抢到锁的记得释放锁
	defer func() {
		dl.Unlock()
	}
	
	// 做真正的任务
	DoJob()
}

以上就是Golang实现可重入锁的示例代码的详细内容!

Golang 相关文章推荐
go语言中切片与内存复制 memcpy 的实现操作
Apr 27 Golang
goland 清除所有的默认设置操作
Apr 28 Golang
Golang 如何实现函数的任意类型传参
Apr 29 Golang
浅谈Golang 切片(slice)扩容机制的原理
Jun 09 Golang
Golang的继承模拟实例
Jun 30 Golang
修改并编译golang源码的操作步骤
Jul 25 Golang
Go语言特点及基本数据类型使用详解
Mar 21 Golang
Go语言安装并操作redis的go-redis库
Apr 14 Golang
Golang获取List列表元素的四种方式
Apr 20 Golang
Golang 并发编程 SingleFlight模式
Apr 26 Golang
Golang实现可重入锁的示例代码
May 25 Golang
GoFrame gredis缓存DoVar Conn连接对象 自动序列化GoFrame gredisDo/DoVar方法Conn连接对象自动序列化/反序列化总结
Jun 14 Golang
Go web入门Go pongo2模板引擎
May 20 #Golang
Go语言入门exec的基本使用
May 20 #Golang
Golang并发工具Singleflight
May 06 #Golang
深入理解 Golang 的字符串
May 04 #Golang
Golang入门之计时器
May 04 #Golang
Golang 入门 之url 包
May 04 #Golang
Golang解析JSON对象
Apr 30 #Golang
You might like
浏览器预览PHP文件时顶部出现空白影响布局分析原因及解决办法
2013/01/11 PHP
php实现快速排序的三种方法分享
2014/03/12 PHP
ThinkPHP公共配置文件与各自项目中配置文件组合的方法
2014/11/24 PHP
php传值赋值和传地址赋值用法实例分析
2015/06/20 PHP
PHP实现上传文件并存进数据库的方法
2015/07/16 PHP
JS 获取span标签中的值的代码 支持ie与firefox
2009/08/24 Javascript
javascript innerText和innerHtml应用
2010/01/28 Javascript
javascript oop开发滑动(slide)菜单控件
2010/08/25 Javascript
精选的10款用于构建良好易用性网站的jQuery插件
2011/01/23 Javascript
回车直接实现点击某按钮的效果即触发单击事件
2014/02/27 Javascript
JQuery实现带排序功能的权限选择实例
2015/05/18 Javascript
jQuery图片加载失败替换默认图片方法汇总
2017/11/29 jQuery
浅谈Vue.js中ref ($refs)用法举例总结
2017/12/19 Javascript
vue利用axios来完成数据的交互
2018/03/23 Javascript
JS实现字符串翻转的方法分析
2018/08/31 Javascript
关于在vue 中使用百度ueEditor编辑器的方法实例代码
2018/09/14 Javascript
vue 之 css module的使用方法
2018/12/04 Javascript
基于Vue插入视频的2种方法小结
2019/04/02 Javascript
解决js中的setInterval清空定时器不管用问题
2020/11/17 Javascript
跟老齐学Python之折腾一下目录
2014/10/24 Python
使用Python来开发Markdown脚本扩展的实例分享
2016/03/04 Python
python里使用正则表达式的组嵌套实例详解
2017/10/24 Python
Python中多个数组行合并及列合并的方法总结
2018/04/12 Python
keras中的History对象用法
2020/06/19 Python
keras的backend 设置 tensorflow,theano操作
2020/06/30 Python
python获得命令行输入的参数的两种方式
2020/11/02 Python
美国在线购买空气净化器、除湿器、加湿器网站:AllergyBuyersClub
2021/03/16 全球购物
冰淇淋店创业计划书范文
2013/12/27 职场文书
写演讲稿要注意的六件事
2014/01/14 职场文书
土地转让协议书
2014/09/27 职场文书
党员检讨书范文
2014/12/27 职场文书
二年级语文下册复习计划
2015/01/19 职场文书
2015年计生工作总结范文
2015/04/24 职场文书
小学教研工作总结2015
2015/05/13 职场文书
《敬重卑微》读后感3篇
2019/11/26 职场文书
vue+elementUI实现表格列的显示与隐藏
2022/04/13 Vue.js