分布式锁的原理和实现(Go)

文章目录

  • 为什么需要分布式锁?
  • go语言分布式锁的实现
    • Redis
      • 自己的实现
      • 红锁是什么
      • 别人的带红锁的实现
    • etcd
    • zk的实现
  • 面试问题
    • 什么是分布式锁?你用过分布式锁吗?
    • 你使用的分布式锁性能如何,可以优化吗?
    • 怎么用Redis来实现一个分布式锁?
    • 怎么确定分布式锁的过期时间?
    • 如果分布式锁过期了,但是业务还没有执行完毕,怎么办?
    • 加锁的时候得到了超时响应,怎么办?
    • 加锁的时候如果锁被人持有了,这时候怎么办?
    • 分布式锁为什么要续约?续约失败了怎么办?如果重试一直都失败,怎么办?
    • 怎么减少分布式锁竞争?
    • 你知道redlock是什么吗?

为什么需要分布式锁?

保证分布式系统并发请求或不同服务实例操作共享资源的安全性,确保在同一时间内,仅有一个进程能够修改共享资源,例如数据库记录或文件,主要用于解决分布式环境中的数据一致性和并发控制问题。 应用场景:用户下单,库存扣减,余额扣减。我们的场景:防止用户信息多写,防止重复发送邮件,防止重复设置定时任务。

  • 使用分布式锁可能会对性能产生一定的影响,但这是为了确保数据的一致性和正确性所必需的;如果操作是操作是幂等的(即使多次执行也会产生相同的结果),可能不需要分布式锁

在这里插入图片描述

go语言分布式锁的实现

Redis

https://github.com/zeromicro/go-zero go-zero里已经实现了redislock,但没有续约机制

自己的实现

// 需要实现的能力
// 1.排他性、原子性
// 2.主动释放/自动释放
// 3.可重入
// 4.可续约

package r_lc

import (
	"context"
	"errors"
	"github.com/go-redis/redis/v8"
	"time"

	"github.com/google/uuid"
)

// RLc 基于redis的分布式锁
type RLc struct {
	rdb *redis.Client
	// key 锁标识
	key string

	// lcTag 唯一标识,防止串锁
	lcTag string

	// expiresIn 过期时间
	expiresIn time.Duration

	// releaseCh 锁释放信号 (看门狗)
	releaseCh chan struct{}

	// RetryInterval LockWait重试锁的间隔。默认100ms
	RetryInterval time.Duration

	// RenewInterval 续约锁间隔,默认为expiresIn/2
	RenewInterval time.Duration

	// MaxRenewDur 自动续约最长时间。默认1小时,当expiresIn大于1小时,为expiresIn
	MaxRenewDur time.Duration
}

type RlcOpt func(lc *RLc)

const (
	retryIntervalDefault = 100 * time.Millisecond
	maxRenewDurDefault   = time.Hour
)

// LUA脚本
var (
	// tryLockLua
	// return 0. 加锁失败
	// return >0. 加锁成功,当前锁的数量
	tryLockLua = `
local key = KEYS[1]
local val = ARGV[1]
local expiresIn = ARGV[2]

-- 锁不存在,加锁
if redis.call('EXISTS', key) == 0 then
	redis.call('HINCRBY', key, val, 1)
	redis.call('PEXPIRE', key, expiresIn)
	return 1
end

-- 锁存在,判断持有锁,增加加锁次数 (可重入)
if redis.call('HEXISTS', key, val) == 1 then
    return redis.call('HINCRBY', key, val, 1)
end

-- 锁被其他进程占用
return 0

`

	// unlockLua
	// return > 0. 剩余待解锁次数
	// return = 0. 解锁成功
	// return = -1. 锁不存在 | 未持有锁
	unlockLua = `
local key = KEYS[1]
local val = ARGV[1]

-- 锁不存在或未持有锁
if redis.call('HEXISTS', key, val) == 0 then
	return -1
end

-- 按次数解锁
local count = redis.call('HINCRBY', key, val, -1)
if count <= 0 then
	-- 全部解锁
	redis.call("DEL",key)
	return 0
end

-- 剩余待解锁次数
return count
`
	// renewLua
	// return 0. 续约失败
	// return 1. 续约成功
	renewLua = `
local key = KEYS[1]
local val = ARGV[1]
local expiresIn = ARGV[2]

-- 锁不存在或未持有锁
if redis.call('HEXISTS', key, val) == 0 then
	return 0
end

-- 设置过期时间
return redis.call('PEXPIRE', key, expiresIn)
`
)

var (
	ErrLostKey = errors.New("lost key") // 锁不存在或被其他进程占用
)

func NewRLc(rdb *redis.Client, key string, expiresIn time.Duration, opts ...RlcOpt) *RLc {
	lc := &RLc{
		rdb:       rdb,
		key:       key,
		lcTag:     uuid.New().String(),
		expiresIn: expiresIn,
		releaseCh: make(chan struct{}),
	}
	for _, opt := range opts {
		opt(lc)
	}
	if lc.RetryInterval == 0 {
		lc.RetryInterval = retryIntervalDefault
	}
	if lc.RenewInterval == 0 {
		lc.RenewInterval = lc.expiresIn / 2
	}
	if lc.MaxRenewDur == 0 {
		lc.MaxRenewDur = maxRenewDurDefault
		if lc.MaxRenewDur < lc.expiresIn {
			lc.MaxRenewDur = lc.expiresIn
		}
	}

	return lc
}

// TryLock 尝试锁
func (lc *RLc) TryLock(ctx context.Context) (lcNum int, res bool) {
	lua := redis.NewScript(tryLockLua)
	lcNum, _ = lua.Run(ctx, lc.rdb, []string{lc.key}, lc.lcTag, lc.expiresIn.Milliseconds()).Int()
	if lcNum == 0 {
		return 0, false
	}
	return lcNum, true
}

// LockWait 尝试锁并等待
func (lc *RLc) LockWait(ctx context.Context, wait time.Duration) (lcNum int, res bool) {
	ctx, cancel := context.WithTimeout(ctx, wait)
	defer cancel()

jumpEnd:
	for {
		select {
		case <-ctx.Done():
			break jumpEnd
		case <-lc.releaseCh:
			break jumpEnd
		default:
			lcNum, res = lc.TryLock(ctx)
			if res {
				return
			}
			time.Sleep(lc.RetryInterval)
		}
	}

	return 0, false
}

// Unlock 解锁
func (lc *RLc) Unlock(ctx context.Context) (leftLcNum int, err error) {
	lua := redis.NewScript(unlockLua)
	leftLcNum, err = lua.Run(ctx, lc.rdb, []string{lc.key}, lc.lcTag).Int()
	if err != nil {
		return 0, err
	}
	if leftLcNum < 0 {
		return 0, ErrLostKey
	}
	if leftLcNum == 0 {
		close(lc.releaseCh)
	}
	return
}

// Renew 续约
// expiresIn=0时,会使用初始化时设定的expiresIn
func (lc *RLc) Renew(ctx context.Context) {

	// 限制续约最大持续时间,减少协程泄露影响
	ctx, cancel := context.WithTimeout(ctx, lc.MaxRenewDur)

	// 续约
	go func(lc *RLc) {
		for {
			select {
			case <-ctx.Done():
				return
			case <-lc.releaseCh:
				cancel()
				return
			default:
				lua := redis.NewScript(renewLua)
				res, _ := lua.Run(ctx, lc.rdb, []string{lc.key}, lc.lcTag, lc.expiresIn.Milliseconds()).Int()
				if res == 0 {
					cancel()
				}
				time.Sleep(lc.RenewInterval)
			}
		}
	}(lc)

	return
}

单元测试

package r_lc

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
	"runtime"
	"testing"
	"time"
)

func getRdb() (rdb *redis.Client, err error) {
	rdb = redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "",
		DB:       1,
	})

	_, err = rdb.Ping(context.TODO()).Result()
	if err != nil {
		return
	}
	return
}

func TestLockWait(t *testing.T) {
	rdb, err := getRdb()
	if err != nil {
		t.Fatal(err)
	}

	t.Run("lock1", func(t1 *testing.T) {
		go func() {
			lockWaitFunc(t1, rdb, 10*time.Second)
		}()
	})

	time.Sleep(10 * time.Millisecond)

	t.Run("lock2", func(t2 *testing.T) {
		go func() {
			lockWaitFunc(t2, rdb, 5*time.Second)
		}()
	})

	fmt.Println("0s NumGoroutine", runtime.NumGoroutine())
	time.Sleep(3 * time.Second)
	fmt.Println("3s NumGoroutine", runtime.NumGoroutine()) // 续约协程启动
	time.Sleep(2 * time.Second)
	fmt.Println("5s NumGoroutine", runtime.NumGoroutine()) // lock2 续约协程释放
	time.Sleep(5 * time.Second)
	fmt.Println("10s NumGoroutine", runtime.NumGoroutine()) // lock1 续约协程保持
	time.Sleep(5 * time.Second)
	fmt.Println("15s NumGoroutine", runtime.NumGoroutine()) // lock1 续约协程释放
	time.Sleep(2 * time.Second)

}

func TestRetry(t *testing.T) {
	rdb, err := getRdb()
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()

	// 初始化锁信息
	lc := NewRLc(rdb, "test-lock", 5*time.Second, func(lc *RLc) {
		lc.MaxRenewDur = time.Second * 10
		lc.RenewInterval = time.Second * 5
		lc.RetryInterval = 10 * time.Millisecond
	})
	lc2 := NewRLc(rdb, "test-lock", 5*time.Second, func(lc *RLc) {
		lc.MaxRenewDur = time.Second * 10
		lc.RenewInterval = time.Second * 5
		lc.RetryInterval = 10 * time.Millisecond
	})
	// 启动续约
	lc.Renew(ctx)

	fmt.Println(lc.TryLock(ctx))
	fmt.Println(lc.TryLock(ctx))
	fmt.Println(lc.TryLock(ctx))
	time.Sleep(5 * time.Second)
	fmt.Println("wwwww")
	fmt.Println(lc2.LockWait(ctx, 10*time.Second))

	fmt.Println(lc.Unlock(ctx))
	fmt.Println(lc.Unlock(ctx))
	fmt.Println(lc.Unlock(ctx))
	fmt.Println(lc.Unlock(ctx))
	fmt.Println(lc.Unlock(ctx))
	fmt.Println(lc.Unlock(ctx))

	return
}

func lockWaitFunc(t *testing.T, rdb *redis.Client, wait time.Duration) {
	ctx := context.Background()

	// 初始化锁信息
	lc := NewRLc(rdb, "test-lock", 15*time.Second)

	// 阻塞式获取锁
	_, getLock := lc.LockWait(ctx, wait)
	if getLock == false {
		fmt.Println("获取锁超时")
		t.Log("获取锁超时")
		return
	}
	defer lc.Unlock(ctx)

	// 启动续约
	lc.Renew(ctx)

	// 处理业务代码
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
	}

	return
}

使用分布式锁

	// 加分布式锁
	contxt := ctx.GetSpanCtx()
	lc := r_lc.NewRLc(server.GetRedisClient(), config.GetDistributedLockKey(fmt.Sprintf("user_info:%s", wsId)), 30*time.Second)
	_, lcRes := lc.LockWait(contxt, 30*time.Second)
	if lcRes == false {
		err = my_err.WrapF(err, Err.ErrCodeSysRequestTimeout, "user_info lc.LockWait wsId:%s", wsId)
		return
	}
	defer lc.Unlock(contxt)
	lc.Renew(contxt)

测试结果,可以使用压测工具

在这里插入图片描述

红锁是什么

红锁算法(Redlock)是一种分布式锁的实现算法,由 Redis 的作者 Antirez 发布。它主要用于解决分布式环境下的资源争用问题,同时保证锁的可靠性和安全性。红锁算法通过在多个 Redis 节点上创建锁,要求获得锁的客户端必须在大多数节点上成功创建锁,从而确保在分布式环境中只有一个客户端可以获得锁。

红锁算法的基本步骤如下:

  1. 客户端获取当前系统时间。
  2. 客户端尝试在 N 个 Redis 节点上创建锁,设置锁的过期时间为过期时间加上一个小的时延。
  3. 如果客户端在大多数节点上成功创建了锁(N/2+1),则认为客户端获得了锁。客户端应将锁的有效期设置为从步骤1开始计算的实际过期时间。
  4. 如果客户端未能在大多数节点上创建锁,那么客户端需要删除在其他节点上创建的锁,并等待一段随机时间后重新尝试。

别人的带红锁的实现

https://juejin.cn/post/7148391514966589477

etcd

todo 待研究

库:https://github.com/etcd-io/etcd

https://juejin.cn/post/7148391514966589477

https://www.liwenzhou.com/posts/Go/etcd/

zk的实现

todo 待研究

库:https://github.com/samuel/go-zookeeper

zookeeper简称zk,zk是通过生成临时有序节点来实现分布式锁的,首先会在/lock目录下一个临时有序节点,后续请求会在节点后面继续创建临时节点。新的子节点后面,会添加一个次序编号,这个生成的编号,会在上一次的编号进行 +1 操作。

zk节点监听机制:每个线程抢占锁之前,先尝试创建自己的ZNode。同样,释放锁的时候,就需要删除创建的Znode。创建成功后,如果不是排号最小的节点,就处于等待通知的状态。等谁的通知呢?不需要其他人,只需要等前一个Znode的通知就可以了。前一个Znode删除的时候,会触发Znode事件,当前节点能监听到删除事件,就是轮到了自己占有锁的时候。第一个通知第二个、第二个通知第三个依次向后。

zk临时节点自动删除:当我们客户端断开连接之后,我们出创建的临时节点会进行自动删除操作,所以我们在使用分布式锁的时候,一般都是会去创建临时节点,这样可以避免因为网络异常等原因,造成的死锁。

面试问题

什么是分布式锁?你用过分布式锁吗?

你使用的分布式锁性能如何,可以优化吗?

怎么用Redis来实现一个分布式锁?

怎么确定分布式锁的过期时间?

如果分布式锁过期了,但是业务还没有执行完毕,怎么办?

加锁的时候得到了超时响应,怎么办?

加锁的时候如果锁被人持有了,这时候怎么办?

分布式锁为什么要续约?续约失败了怎么办?如果重试一直都失败,怎么办?

怎么减少分布式锁竞争?

你知道redlock是什么吗?

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CHI Read传输——CHI(3)

目录 一、Read操作概览 二、DMT(Direct Memory Transfer) 三、DCT (Direct Cache Transfer) 四、without Direct Data Transfer 五、ReadNoSnp and ReadOnce* structure with DMT 本篇我们来介绍一下CHI传输类型中的Read 一、Read操作概览 read操作有以下几种&#xff1…

详解CSS(二)

目录 1.背景属性 1.1背景颜色 1.2背景图片 1.3背景平铺 1.4背景位置 1.5背景尺寸 2.圆角矩形 3.元素的显示模式 3.1行内元素/内联元素&#xff08;Inline element&#xff09; 3.2块级元素&#xff08;Block-level element&#xff09; 3.3行内块元素&#xff08;In…

css-垂直居中的几种写法

图示 1、使用line-height属性&#xff08;当div有固定高度时&#xff09; 2、使用flexbox布局

AGV与智能仓储的应用案例

背景介绍 该企业的智能工厂专注于高端家用电器的生产与研发&#xff0c;包括电子坐便盖、电子坐便器、吸尘器、洗碗机等&#xff0c;覆盖8条关键产线。面对日益增长的市场需求和生产节奏的加快&#xff0c;传统的物流方式已无法满足高效、精准的生产要求。为此&#xff0c;企业…

报名倒计时!「飞天技术沙龙-CentOS 迁移替换专场」参会指南

为帮助广大用户诊断 CentOS 迁移替换过程中的疑难杂症&#xff0c;「飞天技术沙龙-CentOS 迁移替换专场」将于 5 月 29 日&#xff08;周三&#xff09;在北京举办&#xff0c;将围绕如何在确保服务的连续性和稳定性的前提下实现平滑迁移及如何最大限度地利用现有资源前提下确保…

【LeetCode】【9】回文数(1047字)

文章目录 [toc]题目描述样例输入输出与解释样例1样例2样例3 提示进阶Python实现 个人主页&#xff1a;丷从心 系列专栏&#xff1a;LeetCode 刷题指南&#xff1a;LeetCode刷题指南 题目描述 给一个整数x&#xff0c;如果x是一个回文整数&#xff0c;返回true&#xff1b;否…

春秋云境CVE-2018-7422

简介 WordPress Plugin Site Editor LFI 正文 1.进入靶场 2.漏洞利用 /wp-content/plugins/site-editor/editor/extensions/pagebuilder/includes/ajax_shortcode_pattern.php?ajax_path/../../../../../../flag看别人wp做的。不懂怎么弄的&#xff0c;有没有大佬讲一下的

科技引领未来:高速公路可视化

高速公路可视化监控系统利用实时视频、传感器数据和大数据分析&#xff0c;通过图扑 HT 可视化展示交通流量、车速、事故和路况信息。交通管理人员可以实时监控、快速响应突发事件&#xff0c;并优化交通信号和指挥方案。这一系统不仅提高了道路安全性和车辆通行效率&#xff0…

由于找不到d3dx9_39.dll,无法继续执行代码的5种解决方法

在现代科技发展的时代&#xff0c;电脑已经成为我们生活中不可或缺的一部分。然而&#xff0c;由于各种原因&#xff0c;我们可能会遇到一些电脑问题&#xff0c;其中之一就是“d3dx9_39.dll丢失”。这个问题可能会导致我们在运行某些游戏或应用程序时遇到错误提示&#xff0c;…

第53期|GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

07_Servlet

Servlet 一 Servlet简介 1.1 动态资源和静态资源 静态资源 无需在程序运行时通过代码运行生成的资源,在程序运行之前就写好的资源. 例如:html css js img ,音频文件和视频文件 动态资源 需要在程序运行时通过代码运行生成的资源,在程序运行之前无法确定的数据,运行时动态生成…

洛谷 CF1209D Cow and Snacks

题目来源于&#xff1a;洛谷 题目本质&#xff1a;并查集 解题思路&#xff1a; 我们以每种化为一个点&#xff0c;以每个客人喜欢的两朵花给两朵花连一条无向边。则会出现一定数目的连通块&#xff0c;连通块总个数为 ans。 对每个连通块进行分析&#xff1a;第一个客人买走…

重大新闻! AUS GLOBAL 上线积分商城

AUS Global Mall&#xff1a;概述 AUS Global Mall是由AUS Global &#xff0c;一家外汇经纪公司推出的令人兴奋的新在线商店。作为一个尊贵的客户&#xff0c;你现在可以获得广泛的产品和服务&#xff0c;可以通过积分兑换。通过AUS Global Mall&#xff0c;我们旨在为您提供…

软考高项 各章节知识点【细】

文章目录 前五章项目管理概论项目立项管理项目整合管理范围管理进度管理成本管理质量管理资源管理沟通管理风险管理采购管理干系人管理绩效域配置与变更管理招投标、政府采购 前五章 数字经济是继农业经济、工业经济之后的主要经济形态&#xff0c;是以数据资源为关键要素&…

深入解析淘宝详情api接口

一、淘宝详情api接口简介 淘宝详情api接口是淘宝开放平台提供的一种商品详情数据接口&#xff0c;允许开发者通过调用该接口获取淘宝平台上商品的详细信息&#xff0c;包括商品标题、描述、价格、库存、销量、评价等。联讯数据该接口为开发者提供了丰富的商品数据&#xff0c;…

RabbitMQ 之 死信队列

目录 ​编辑一、死信的概念 二、死信的来源 三、死信实战 1、代码架构图 2、消息 TTL 过期 &#xff08;1&#xff09;消费者 &#xff08;2&#xff09;生产者 &#xff08;3&#xff09;结果展示​编辑 3、队列达到最大长度 &#xff08;1&#xff09;消费者 &…

雷军-2022.8小米创业思考-8-和用户交朋友,非粉丝经济;性价比是最大的诚意;新媒体,直播离用户更近;用真诚打动朋友,脸皮厚点!

第八章 和用户交朋友 2005年&#xff0c;为了进一步推动金山的互联网转型&#xff0c;让金山的同事更好地理解互联网的精髓&#xff0c;我推动了一场向谷歌学习的运动&#xff0c;其中一个小要求就是要能背诵“谷歌十诫”。 十诫的第一条就令人印象深刻&#xff1a;以用户为中…

rfid资产管理系统如何帮助医院管理耗材的

RFID资产管理系统可以帮助医院管理耗材&#xff0c;提高耗材管理的效率和准确性。以下是它可以发挥作用的几个方面&#xff1a; 1. 实时跟踪和定位&#xff1a;使用RFID标签附加在耗材上&#xff0c;可以实时跟踪和定位耗材的位置。医院可以通过系统查询耗材的实时位置&#xf…

“AI黏土人”一夜爆火,图像生成类应用应该如何长期留住用户?

文章目录 最近大火的“AI黏土人”&#xff0c;一股浓浓的《小羊肖恩》风。 凭借这这种搞怪的风格&#xff0c;“AI黏土人”等图像生成类应用凭借其创新技术和市场需求迅速崛起并获得巨大关注。然而&#xff0c;要保持用户黏性并确保长期发展&#xff0c;这些应用需要采取一系列…

Python爬虫项目实战:百度任意图片抓取

大家好&#xff0c;我是南枫&#xff0c;这篇文章我将给大家介绍如何使用Python爬虫来达到想爬哪个明星图片就能爬下来的效果&#xff0c;那我们接下来看看如何实现的吧。 导入Python的requests库和re库。requests库用于发送HTTP请求&#xff0c;而re库用于处理正则表达式。 通…