Golang实现分布式锁

news2024/11/17 0:55:17

1. go实现分布式锁

通过 golang 实现一个简单的分布式锁,包括锁续约、重试机制、singleflght机制的使用

1.1 redis_lock.go

package redis_lock

import (
	"context"
	_ "embed"
	"errors"
	"github.com/go-redis/redis/v9"
	"github.com/google/uuid"
	"golang.org/x/sync/singleflight"
	"time"
)

// go:embed 可以直接解析出文件中的字符串
var (
	//go:embed lua_unlock.lua
	luaUnlock string
	//go:embed refresh.lua
	luaRefresh string
	//go:embed lock.lua
	luaLock string
	//定义好两个异常信息
	ErrLockNotHold         = errors.New("未持有锁")
	ErrFailedToPreemptLock = errors.New("加锁失败")
)

type Client struct {
	//采用公共的接口,后续实例通过传入的方式
	client redis.Cmdable
	// singleflight 用于在一个实例的多个携程中只需要竞争出一个携程
	s singleflight.Group
}

func NewClient(c redis.Cmdable) *Client {
	return &Client{
		client: c,
	}
}

func (c *Client) SingleflightLock(ctx context.Context,
	key string,
	expire time.Duration,
	retry RetryStrategy,
	timeout time.Duration) (*Lock, error) {
	for {
		flag := false
		resCh := c.s.DoChan(key, func() (interface{}, error) {
			flag = true
			return c.Lock(ctx, key, expire, retry, timeout)
		})
		select {
		case res := <-resCh:
			if flag {
				if res.Err != nil {
					return nil, res.Err
				}
				//返回锁对象
				return res.Val.(*Lock), nil
			}
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}
}

//Lock 加锁方法,根据重试机制进行重新获取
func (c *Client) Lock(ctx context.Context,
	key string,
	expire time.Duration,
	retry RetryStrategy,
	timeout time.Duration) (*Lock, error) {
	var timer *time.Timer
	defer func() {
		if timer != nil {
			timer.Stop()
		}
	}()
	for {
		//设置超时
		lct, cancel := context.WithTimeout(ctx, timeout)
		//获取到uuid
		value := uuid.New().String()
		//执行lua脚本进行加锁
		result, err := c.client.Eval(lct, luaLock, []string{key}, value, expire).Bool()
		//用于主动释放资源
		cancel()
		if err != nil && !errors.Is(err, context.DeadlineExceeded) {
			return nil, err
		}
		if result {
			return newLock(c.client, key, value), nil
		}
		//可以不传重试机制
		if retry != nil {
			//通过重试机制获取重试的策略
			interval, ok := retry.Next()
			if !ok {
				//不用重试
				return nil, ErrFailedToPreemptLock
			}
			if timer == nil {
				timer = time.NewTimer(interval)
			}
			timer.Reset(interval)
			select {
			case <-timer.C: //睡眠时间超时了
				return nil, ctx.Err()
			case <-ctx.Done(): //整个调用的超时
				return nil, ctx.Err()
			}
		}
	}
}

// TryLock 尝试加锁
func (c *Client) TryLock(ctx context.Context, key string, expire time.Duration) (*Lock, error) {
	return c.Lock(ctx, key, expire, nil, 0)
}

// NewLock 创建一个锁结构体
func newLock(client redis.Cmdable, key string, value string) *Lock {
	return &Lock{
		client:     client,
		key:        key,
		value:      value,
		unLockChan: make(chan struct{}, 1), //设置1个缓存数据,用于解锁的信号量
	}
}

// Lock 结构体对象
type Lock struct {
	client redis.Cmdable
	key    string
	value  string
	expire time.Duration
	//在解锁成功之后发送信号来取消续约
	unLockChan chan struct{}
}

// AutoRefresh 自动续约
func (l *Lock) AutoRefresh(interval time.Duration, timeout time.Duration) error {
	//设计一个管道,如果失败了,就发送数据到管道之中,通知进行重试
	retry := make(chan struct{}, 1)
	//方法返回时关闭close
	defer close(retry)
	ticker := time.NewTicker(interval)
	for {
		select {
		//接收到结束的信号时,直接return
		case <-l.unLockChan:
			return nil
		//监听重试的管道
		case <-retry:
			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			err := l.Refresh(ctx)
			//主动调用释放资源
			cancel()
			if err == context.DeadlineExceeded {
				// 执行重试往管道中发送一个信号
				retry <- struct{}{}
				continue
			}
			if err != nil {
				return err
			}
		case <-ticker.C:
			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			err := l.Refresh(ctx)
			//主动调用释放资源
			cancel()
			if err == context.DeadlineExceeded {
				// 执行重试往管道中发送一个信号
				retry <- struct{}{}
				continue
			}
			if err != nil {
				return err
			}
		}
	}
}

// Refresh 续约
func (l *Lock) Refresh(ctx context.Context) error {
	//执行lua脚本,对锁进行续约
	i, err := l.client.Eval(ctx, luaRefresh, []string{l.key}, l.value, l.expire.Milliseconds()).Int64()
	if err == redis.Nil {
		return ErrLockNotHold
	}
	if err != nil {
		return err
	}
	if i == 0 {
		return ErrLockNotHold
	}
	return nil
}

// Unlock 解锁
func (l *Lock) Unlock(ctx context.Context) error {
	//解锁时,退出方法需要发送一个信号让自动续约的goroutine停止
	defer func() {
		l.unLockChan <- struct{}{}
		close(l.unLockChan)
	}()
	//判断返回的结果
	result, err := l.client.Eval(ctx, luaUnlock, []string{l.key}, l.value).Int64()
	if err == redis.Nil {
		return ErrLockNotHold
	}
	if err != nil {
		return err
	}
	//lua脚本返回的结果如果为0,也是代表当前锁不是自己的
	if result == 0 {
		return ErrLockNotHold
	}
	return nil
}

1.2 retry.go

package redis_lock

import "time"

// RetryStrategy 重试策略
type RetryStrategy interface {
	// Next 下一次重试的时间是多久,返回两个参数 time 时间,bool 是否直接重试
	Next() (time.Duration, bool)
}

1.3 lock.lua

lua脚本原子化加锁

--[[ 获取到对应的value是否跟当前的一样 ]]
if redis.call("get", KEYS[1]) == ARGV[1]
then
-- 如果一样直接对其时间进行续约
	return redis.call("pexpire", KEYS[1], ARGV[2])
else
-- 如果不一样调用setnx命令对其进行设置值
	return redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])

1.4 lua_unlock.lua

lua脚本原子化解锁

if redis.call("get", KEYS[1]) == ARGV[1] then
	-- 返回0,代表key不在
	return redis.call("del", KEYS[1])
else
	-- key在,但是值不对
	return 0
end

1.5 refresh.lua

lua脚本续约

if redis.call("get", KEYS[1]) == ARGV[1] then
	-- 返回0,代表key不在
	return redis.call("pexpire", KEYS[1], ARGV[2])
else
	-- key在,但是值不对
	return 0
end

1.6 单元测试

使用go-mock工具生成本地的单元测试,不需要再单独的搭建一个 redis 的服务端

项目根目录下安装mockgen工具

go install github.com/golang/mock/mockgen@latest

添加依赖

go get github.com/golang/mock/mockgen/model

生成redis客户端接口

mockgen -package=mocks -destination=mocks/redis_cmdable.mock.go github.com/go-redis/redis/v9 Cmdable

  • package:指定包
  • destination:生成路径名称
  • 剩下的是指定使用redis包下面的 Cmdable接口生成代码

在这里插入图片描述

测试类

func TestClient_TryLock(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	testCase := []struct {
		//测试的场景
		name string
		//输入
		key        string
		expiration time.Duration
		//返回一个mock数据
		mock func() redis.Cmdable
		//期望的返回的错误值
		wantError error
		//期望返回的锁
		wantLock *Lock
	}{
		{
			name:       "locked",
			key:        "locked-key",
			expiration: time.Minute,
			mock: func() redis.Cmdable {
				rdb := mocks.NewMockCmdable(ctrl)
				res := redis.NewBoolResult(true, nil)
				i := []interface{}{gomock.Any(), time.Minute}
				rdb.EXPECT().Eval(gomock.Any(), luaLock, []string{"locked-key"}, i...).Return(res)
				return rdb
			},
			wantLock: &Lock{
				key: "locked-key",
			},
		},
	}

	for _, tc := range testCase {
		t.Run(tc.name, func(t *testing.T) {
			var c = NewClient(tc.mock())
			l, err := c.TryLock(context.Background(), tc.key, tc.expiration)
			assert.Equal(t, tc.wantError, err)
			if err != nil {
				return
			}
			//判断返回的key是否跟期望的一样
			assert.Equal(t, tc.key, l.key)
			assert.Equal(t, tc.wantLock.key, l.key)
			assert.NotEmpty(t, l.value)
		})
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/150013.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java Netty框架自建DNS代理服务器教程

前言 DNS协议作为着互联网客户端-服务器通信模式得第一关&#xff0c;在当下每天都有成千上亿上网记录产生得当今社会&#xff0c;其重要性自然不可言喻。在国内比较有名得DNS服务器有电信得114.114.114.114、阿里云得223.5.5.5&#xff0c;DNSPod得119.29.29.29&#xff0c;配…

【矩阵论】8. 常用矩阵总结——镜面阵,正定阵

8.4 镜面阵 法向量确定一个镜面 8.4.1 镜面阵的作用 对法向量 Aα−αA\alpha-\alphaAα−αA(Aα)A2ααA(A\alpha)A^2\alpha\alphaA(Aα)A2αα 对镜面上向量 AYYAYYAYY 8.4.2 镜面阵表示 AIn−2ααH∣α∣2,其中α(x1x2⋮xn)∈Cn,且α≠0AI_n-\frac{2\alpha\alpha^H}{\…

【实际开发02】- 同模块 - 单表 CRUD 代码 - 批量操作

目录 0. yml 配置 1. 账号 / 密码 等有概率变更的信息 推荐 动态配置 , 避免写死 1. entity 处理 ( 减少后续 insert/update 判空处理 ) 1. volidation.annotation 配合 Valid - 参数校验 2. Validated - ( 相较于 valid 更加严谨的校验 ) 1. Save / Status 2. Update /…

vue中使用axios

一、axios axios是一个基于Promise的网络请求库。既可以在node.js&#xff08;服务器端&#xff09;使用&#xff0c;也可以在浏览器端使用 1. 在node.js中使用的原生的http模块 2. 在浏览器中使用的XMLHttpRequest 二、vue中的使用方法 1. 安装&#xff1a;npm install axios…

二分查找及其扩展

目录 二分查找算法思想&#xff1a; 循环 递归 有多个与key相等数据的查询&#xff0c;找最左边与关键字相等的那个 找第一个大于key的元素的下标 有序循环数组的二分查找 二分查找算法思想&#xff1a; 二分查找也叫折半查找&#xff0c;查找效率较高。但是查找的线性表…

振弦采集模块电子标签测量(智能振弦传感器)

振弦采集模块电子标签测量&#xff08;智能振弦传感器&#xff09; 此功能在 SF3.52 版本时增加。 固件版本 V3.52 修改固件版本号为 V3.52_2201009。 增加了电子标签测量功能。 WKMOD.[12]用于控制是否使用此功能 新增状态位 STATUS.[13]&#xff0c;用来表示是否检测到了电子…

JAVA开发运维(DevOps过程)

DevOps开发运维的一套方法论。这边文章主要借鉴万达的DevOps的建设过程。谈谈DevOps主要解决那些问题和怎么解决。DevOps的是一种IT项目开发管理方法论&#xff0c;它旨在提供全面的持续集成、持续交付等能力&#xff0c;并持在续进行过程度量和改进&#xff0c;不断提升 IT 运…

新电脑安装vmware和centos8系统

1.安装vmware 1.1.vmware官网下载 需要付费 1.2.使用指定破解版本 链接&#xff1a;https://pan.baidu.com/s/1YEeaDyKAQk_3t6ITw2aaTQ 提取码&#xff1a;fjyf vmware16最新许可证密钥&#xff1a; ZF3R0-FHED2-M80TY-8QYGC-NPKYF YF390-0HF8P-M81RQ-2DXQE-M2UT6 ZF71R-DMX…

HTML实现网站欢迎页过渡

演示 一秒后直达主界面 css html, body {background: radial-gradient(#181818, #000000);margin: 0;padding: 0;border: 0;-ms-overflow-style: none;}::-webkit-scrollbar {width: 0.5em;height: 0.5em;background-color: #c7c7c7;}/*定义滚动条轨道 内阴影圆角*/::-webkit…

c++11 标准模板(STL)(std::forward_list)(二)

定义于头文件 <forward_list> template< class T, class Allocator std::allocator<T> > class forward_list;(1)(C11 起)namespace pmr { template <class T> using forward_list std::forward_list<T, std::pmr::polymorphic_…

什么是数字孪生城市

数字孪生城市理念自提出以来不断升温&#xff0c;已成为新型智慧城市建设的热点&#xff0c;受到政府和产业 界的高度关注和认同。 什么是数字孪生城市 北京智汇云舟科技有限公司成立于2012年&#xff0c;专注于创新性的“视频孪生&#xff08;实时实景数字孪生&#xff09;”…

【Java编程进阶】Java抽象类与接口详解

推荐学习专栏&#xff1a;Java 编程进阶之路【从入门到精通】&#xff0c;从入门到就业精通&#xff0c;买不了吃亏&#xff0c;买不了上当&#xff01;&#xff01; 文章目录1. 抽象类2.接口3. 抽象类和接口对比4. 总结Java基础教程系列文章1. 抽象类 前面说到&#xff0c;Ja…

【营销获客三】信贷细分客群研究——小微企业主

【营销获客三】信贷细分客群研究——小微企业主一、小微企业主客群综述1.1 小微企业主定义与体量1.2 小微企业主信贷需求规模1.3 小微企业主的发展历史1.4 小微企业主不同阶段的痛点问题二、小微企业主整体客群特征2.1 客群特征概述2.2 基本属性特征2.3 经营情况特征2.4 融资借…

RTL8380M/RTL8382M管理型交换机系统软件操作指南五:ACL/访问控制列表

接下来将对ACL进行详细的描述&#xff0c;主要包括以下四个方面内容&#xff1a;ACL概述、工作原理、ACL组设置、ACL规则 1.1 ACL概述 访问控制列表&#xff08;Access Control List&#xff0c;ACL&#xff09; 是路由器和交换机接口的指令列表&#xff0c;用来控制端口进出的…

自连接讲解

什么是自连接&#xff1f; 自连接可以理解为自己连接自己&#xff0c;在一张表上面所进行的操作&#xff1b;将一张表分成两张结构和数据完全一样的表&#xff0c;相当于克隆了一张跟自己长得一模一样的表&#xff1b; 但是既然是两张一模一样的表&#xff0c;数据库怎么去区分…

[ 数据结构 ] 汉诺塔--------分治算法最佳实践

0 分治算法 分治法:是一种很重要的算法。字面上的解释是“分而治之”&#xff0c;就是把一个复杂的问题分成两个或更多的相同或相似的子问题&#xff0c;再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解&#xff0c;原问题的解即子问题的解的合并。经典问题:…

什么你还不知道?快来看看我的年度总结吧

作者介绍&#xff1a;阿玥的小东东 作者职位&#xff1a;后端、python、正在学习c 主页&#xff1a;阿玥的小东东 现在呢&#xff0c;人们都在写自己的年度总结&#xff0c;我也来凑个热闹吧&#xff01; 其实我来CSDN这个平台时间不算久&#xff0c;从2022年11月15号开始&…

<C++>set和map

文章目录1. 关联式容器2. 键值对3. 树形结构的关联式容器4. set4.1 set的介绍4.2 set的使用4.2.1 set的模板参数4.2.2 set的构造4.2.3 set的迭代器4.2.4 set的容量4.2.5 set修改操作4.2.6 set的使用举例5. multiset5.1 multiset的介绍5.2 multiset的使用6. map6.1 map的介绍6.2…

字符串相关类

文章目录一、String类String的介绍String实例化面试题&#xff1a;String snew String("abc")创建对象&#xff0c;在内存中创建了几个对象&#xff1f;易错题1易错题2String常用方法String与char[ ]之间的转换String与byte[ ]之间的转换二、StringBuffer类、StringB…

如何将NACOS作为配置中心

新建一个命名空间 点击创建配置 关键点1&#xff1a;Data ID的命名规则&#xff1a; 前面我们演示了在 nacos 控制台新建一个 DataID 为 cloud-producer-server-dev.yaml 的数据集&#xff0c;那么这个 Data ID 是什么呢&#xff1f;Data ID 是配置集的唯一标识&#xff0c;一个…