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:垃圾回收GC触发条件详解
Apr 24 Golang
golang 如何用反射reflect操作结构体
Apr 28 Golang
解决Go gorm踩过的坑
Apr 30 Golang
golang slice元素去重操作
Apr 30 Golang
golang协程池模拟实现群发邮件功能
May 02 Golang
Golang: 内建容器的用法
May 05 Golang
golang switch语句的灵活写法介绍
May 06 Golang
Go timer如何调度
Jun 09 Golang
Go遍历struct,map,slice的实现
Jun 13 Golang
golang 实用库gotable的具体使用
Jul 01 Golang
golang操作redis的客户端包有多个比如redigo、go-redis
Apr 14 Golang
Go结合Gin导出Mysql数据到Excel表格
Aug 05 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
phpcms模块开发之swfupload的使用介绍
2013/04/28 PHP
PHP正则匹配反斜杠'\'和美元'$'的方法
2017/02/08 PHP
JQUERY THICKBOX弹出层插件
2008/08/30 Javascript
原生js实现shift/ctrl/alt按键的获取
2013/04/08 Javascript
使用jQuery内容过滤选择器选择元素实例讲解
2013/04/18 Javascript
js 实现浏览历史记录示例
2014/04/20 Javascript
基于Vue2的移动端开发环境搭建详解
2016/11/03 Javascript
使用JavaScript为一张图片设置备选路径的方法
2017/01/04 Javascript
微信小程序 限制1M的瘦身技巧与方法详解
2017/01/06 Javascript
Javascript基础回顾之(一) 类型
2017/01/31 Javascript
jQuery插件autocomplete使用详解
2017/02/04 Javascript
解决node.js含有%百分号时发送get请求时浏览器地址自动编码的问题
2019/11/20 Javascript
纯JS实现五子棋游戏
2020/05/28 Javascript
.netcore+vue 实现压缩文件下载功能
2020/09/24 Javascript
[09:22]2014DOTA2西雅图国际邀请赛 主赛事第二日TOPPLAY
2014/07/21 DOTA
python中的hashlib和base64加密模块使用实例
2014/09/02 Python
Python最长公共子串算法实例
2015/03/07 Python
可能是最全面的 Python 字符串拼接总结【收藏】
2018/07/09 Python
Python requests库用法实例详解
2018/08/14 Python
Python使用pyautocad+openpyxl处理cad文件示例
2019/07/11 Python
python tkinter组件摆放方式详解
2019/09/16 Python
python实现简易学生信息管理系统
2020/04/05 Python
用CSS3的box-reflect设置文字倒影效果的方法讲解
2016/03/07 HTML / CSS
俄罗斯街头服装品牌:Black Star Wear
2017/03/01 全球购物
Marc Jacobs彩妆官网:Marc Jacobs Beauty
2017/07/03 全球购物
澳大利亚第一的设计师礼服租赁网站:GlamCorner
2017/08/13 全球购物
YOOX台湾:意大利奢侈品电商
2018/10/13 全球购物
耐克奥地利官网:Nike奥地利
2019/08/16 全球购物
英国Lookfantastic中文网站:护肤品美妆美发购物(英国直邮)
2020/04/27 全球购物
网络工程师个人的自我评价范文
2013/10/01 职场文书
财产公证书格式
2014/04/10 职场文书
团日活动总结范文
2014/04/25 职场文书
2015个人半年总结范文
2015/03/09 职场文书
军训结束新闻稿
2015/07/17 职场文书
python中的plt.cm.Paired用法说明
2021/05/31 Python
Python安装及建立虚拟环境的完整步骤
2022/06/25 Servers