2023-06-19:讲一讲Redis分布式锁的实现?

news2024/11/25 20:53:41

2023-06-19:讲一讲Redis分布式锁的实现?

答案2023-06-19:

Redis分布式锁最简单的实现

要实现分布式锁,确实需要使用具备互斥性的Redis操作。其中一种常用的方式是使用SETNX命令,该命令表示"SET if Not Exists",即只有在key不存在时才设置其值,否则不进行任何操作。通过这种方式,两个客户端进程可以执行SETNX命令来实现互斥,从而达到分布式锁的目的。

下面是一个示例:

客户端1申请加锁,加锁成功:

SETNX lock_key 1

客户端2申请加锁,由于它处于较晚的时间,加锁失败:

SETNX lock_key 1

通过这种方式,您可以使用Redis的互斥性来实现简单的分布式锁机制。

image.png

对于加锁成功的客户端,可以执行对共享资源的操作,比如修改MySQL的某一行数据或调用API请求。

操作完成后,需要及时释放锁,以便后续的请求能够访问共享资源。释放锁非常简单,只需使用DEL命令来删除相应的锁键(key)即可。

下面是释放锁的示例逻辑:

DEL lock_key

通过执行以上DEL命令,成功释放锁,以让后续的请求能够获得锁并执行操作共享资源的逻辑。

这样,通过使用SETNX命令进行加锁,然后使用DEL命令释放锁,您就可以实现基本的分布式锁机制。

image.png

但是,它存在一个很大的问题,当客户端 1 拿到锁后,如果发生下面的场景,就会造成「死锁」:

1、程序处理业务逻辑异常,没有及时释放锁。

2、进程崩溃或意外停止,无法释放锁。

在这种情况下,客户端将永远占用该锁,其他客户端将无法获取该锁。如何解决这个问题呢?

如何避免死锁?

当考虑在申请锁时为其设置一个「租期」时,可以在Redis中通过设置「过期时间」来实现。假设我们假设操作共享资源的时间不会超过10秒,在加锁时,可以给该key设置一个10秒的过期时间即可。这样做可以确保在申请锁后的一段时间内,如果锁的持有者在该时间内没有更新锁的过期时间,锁将会自动过期,从而防止锁被永久占用

SETNX lock 1    // 加锁
EXPIRE lock 10  // 10s后自动过期

image.png

这样一来,无论客户端是否异常,这个锁都可以在 10s 后被「自动释放」,其它客户端依旧可以拿到锁。

但现在还是有问题:

当前的操作是将加锁和设置过期时间作为两个独立的命令执行,存在一个问题,即可能只执行了第一条命令而第二条命令却未能及时执行,从而导致问题。例如:

  • SETNX 命令执行成功后,由于网络问题导致 EXPIRE 命令执行失败。

  • SETNX 命令执行成功后,Redis 异常宕机,导致 EXPIRE 命令没有机会执行。

  • SETNX 命令执行成功后,客户端异常崩溃,同样导致 EXPIRE 命令没有机会执行。

总之,这两条命令不能保证是原子操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生「死锁」问题。

幸运的是,在 Redis 2.6.12 版本之后,Redis 扩展了 SET 命令的参数。用这一条命令就可以了:

SET lock 1 EX 10 NX

image.png

锁被别人释放怎么办?

上面的命令执行时,每个客户端在释放锁时,并没有进行严格的验证,存在释放别人锁的潜在风险。为了解决这个问题,可以在加锁时为每个客户端设置一个唯一的标识符(unique identifier),并在解锁时对比标识符来验证是否有权释放锁。

例如,可以是自己的线程 ID,也可以是一个 UUID(随机且唯一),这里我们以UUID 举例:

SET lock $uuid EX 20 NX

之后,在释放锁时,要先判断这把锁是否还归自己持有,伪代码可以这么写:

if redis.get("lock") == $uuid:
    redis.del("lock")

这里释放锁使用的是 GET + DEL 两条命令,这时,又会遇到我们前面讲的原子性问题了。这里可以使用lua脚本来解决。

安全释放锁的 Lua 脚本如下:

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

好了,这样一路优化,整个的加锁、解锁的流程就更「严谨」了。

这里我们先小结一下,基于 Redis 实现的分布式锁,一个严谨的的流程如下:

1、加锁

SET lock_key $unique_id EX $expire_time NX

2、操作共享资源

3、释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再DEL 释放锁

go代码实现分布式锁

package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/google/uuid"
)

const (
	LockTime         = 5 * time.Second
	RS_DISTLOCK_NS   = "tdln:"
	RELEASE_LOCK_LUA = `
        if redis.call('get',KEYS[1])==ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
    `
)

type RedisDistLock struct {
	id          string
	lockName    string
	redisClient *redis.Client
	m           sync.Mutex
}

func NewRedisDistLock(redisClient *redis.Client, lockName string) *RedisDistLock {
	return &RedisDistLock{
		lockName:    lockName,
		redisClient: redisClient,
	}
}

func (this *RedisDistLock) Lock() {
	for !this.TryLock() {
		time.Sleep(100 * time.Millisecond)
	}
}

func (this *RedisDistLock) TryLock() bool {
	if this.id != "" {
		// 处于加锁中
		return false
	}
	this.m.Lock()
	defer this.m.Unlock()
	if this.id != "" {
		// 处于加锁中
		return false
	}
	ctx := context.Background()
	id := uuid.New().String()
	reply := this.redisClient.SetNX(ctx, RS_DISTLOCK_NS+this.lockName, id, LockTime)
	if reply.Err() == nil && reply.Val() {
		this.id = id
		return true
	}

	return false
}

func (this *RedisDistLock) Unlock() {
	if this.id == "" {
		// 未加锁
		panic("解锁失败,因为未加锁")
	}
	this.m.Lock()
	defer this.m.Unlock()
	if this.id == "" {
		// 未加锁
		panic("解锁失败,因为未加锁")
	}
	ctx := context.Background()
	reply := this.redisClient.Eval(ctx, RELEASE_LOCK_LUA, []string{RS_DISTLOCK_NS + this.lockName}, this.id)
	if reply.Err() != nil {
		panic("释放锁失败!")
	} else {
		this.id = ""
	}
}

func main() {

	client := redis.NewClient(&redis.Options{
		Addr: "172.16.11.111:64495",
	})
	const LOCKNAME = "百家号:福大大架构师每日一题"

	lock := NewRedisDistLock(client, LOCKNAME)

	lock.Lock()
	fmt.Println("加锁main")
	ch := make(chan struct{})
	go func() {
		lock := NewRedisDistLock(client, LOCKNAME)
		lock.Lock()
		fmt.Println("加锁go程")
		lock.Unlock()
		fmt.Println("解锁go程")
		ch <- struct{}{}
	}()
	time.Sleep(time.Second * 2)
	lock.Unlock()
	fmt.Println("解锁main")
	<-ch
}


在这里插入图片描述

锁过期时间不好评估怎么办?

image.png

看上面这张图,加入key的失效时间是10s,但是客户端C在拿到分布式锁之后,然后业务逻辑执行超过10s,那么问题来了,在客户端C释放锁之前,其实这把锁已经失效了,那么客户端A和客户端B都可以去拿锁,这样就已经失去了分布式锁的功能了!!!

比较简单的妥协方案是,尽量「冗余」过期时间,降低锁提前过期的概率,但是这个并不能完美解决问题,那怎么办呢?

分布式锁加入看门狗

在加锁过程中,可以设置一个过期时间,并启动一个守护线程(也称为「看门狗」线程),定时检测锁的剩余有效时间。如果锁即将过期,但共享资源操作尚未完成,守护线程可以自动对锁进行续期,重新设置过期时间。

为什么要使用守护线程:

image.png

go中的红锁

package main

import (
	"fmt"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/go-redsync/redsync/v4"
	"github.com/go-redsync/redsync/v4/redis/goredis/v8"
)

func main() {
	client := redis.NewClient(&redis.Options{
		Addr:     "172.16.11.111:64495",
		Password: "", // 如果有密码,请提供密码
		DB:       0,  // 如果使用不同的数据库,请修改为准确的数据库编号
	})

	pool := goredis.NewPool(client)

	const LOCKNAME = "百家号:福大大架构师每日一题"

	redsync := redsync.New(pool)

	mutex := redsync.NewMutex(LOCKNAME)

	if err := mutex.Lock(); err != nil {
		fmt.Println("加锁失败:", err)
		return
	}

	fmt.Println("加锁main")

	ch := make(chan struct{})

	go func() {
		mutex := redsync.NewMutex(LOCKNAME)

		if err := mutex.Lock(); err != nil {
			fmt.Println("加锁失败:", err)
			return
		}

		fmt.Println("加锁go程")
		mutex.Unlock()
		fmt.Println("解锁go程")

		ch <- struct{}{}
	}()

	time.Sleep(time.Second * 2)
	mutex.Unlock()
	fmt.Println("解锁main")

	<-ch
}

在这里插入图片描述

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

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

相关文章

ChatGLM-6B 在 ModelWhale和本地 平台的部署与微调教程

ChatGLM-6B 在 ModelWhale 平台的部署与微调教程 工作台 - Heywhale.com ChatGLM-6B 介绍 ChatGLM-6B 是一个开源的、支持中英双语的对话语言模型&#xff0c;基于 General Language Model (GLM) 架构&#xff0c;具有 62 亿参数。结合模型量化技术&#xff0c;用户可以在消费…

古希腊历史的五个阶段

古希腊&#xff08;Greece&#xff09;&#xff0c;是西方文明的源头之一&#xff0c;是古代巴尔干半岛南部、爱琴海诸岛和小亚细亚沿岸的总称。古希腊是西方文明最重要和直接的渊源。 西方有记载的文学、科技、艺术都是从古代希腊开始的。古希腊不是一个国家的概念&#xff0c…

4.Mysql备份与恢复

文章目录 Mysql备份与恢复重要性数据库备份的分类从物理与逻辑的角度从数据库的备份策略角度 常见的备份方法MySQL 完全备份优点与缺点数据库完全备份分类Mysql物理冷备份与恢复mysqldump备份数据库恢复数据库 mysql日志管理错误日志通用查询日志二进制日志慢查询日志查看日志文…

GAMES101 笔记 Lecture 04 Transformation Cont.

目录 3D Transformations(三维变换)Viewing transformation(观测变换)View/Camera Transformation(视图变换)What is view transformation(什么是视图变换)&#xff1f;How to perform view transformation?(如何进行视图变换呢&#xff1f;) Projection Transformation(投影变…

古希腊简史

古希腊&#xff08;Ancient Greece&#xff09;狭义上指希腊地区从公元前12世纪迈锡尼文明毁灭至公元前146年希腊地区被罗马共和国征服为止。广义上指爱琴诸文明在罗马人征服前的全部历史。 克里特岛文明&#xff1a;早在约公元前3650年&#xff0c;爱琴海地区就孕育了灿烂的米…

理解Web3公链共识算法的原理与机制

Web3时代带来了去中心化、透明和安全的数字经济发展&#xff0c;而公链的共识算法是实现这一目标的关键。共识算法确保了公链网络中的节点对交易和状态的一致性达成共识&#xff0c;同时防止了恶意行为和双重支付等问题。本文将深入探讨Web3公链共识算法的核心原理与机制。 1.共…

《C++ Primer》--学习4

函数 函数基础 局部静态对象 局部静态对象 在程序的执行路径第一次经过对象定义语句时初始化&#xff0c;并且直到程序终止才被销毁&#xff0c;在此期间即使对象所在函数结束执行也不会对它有影响 指针或引用形参与 const main&#xff1a; 处理命令行选项 列表初始化返回…

无源供电无线测温系统的研究应用

摘要&#xff1a;无源供电无线测温在线监测系统是一种基于声表面波技术的测温技术&#xff0c;在变电站监测方面得到了很好的技术实践应用。本文对无源供电无线测温在线监测系统研究应用进行分析研究。 关键词&#xff1a;设备检测&#xff1b;无线测温。 引言 在电力系统设…

【Java-SpringBoot+Vue+MySql】Day1-环境搭建项目创建

目录 一、搭建环境 1、数据库 2、数据库可视化 3、JAVA-JDK 4、项目管理器 &#xff08;1&#xff09;修改仓库路径 &#xff08;2&#xff09;修改镜像 5、编译器 二、创建项目 1、新建项目 2、修改下载源 三、使用LomBok依赖 四、有则改之 1、发现问题 2、解决问…

Docker:如何删除已存在的镜像

要删除已存在的 Docker 镜像&#xff0c;您可以使用 docker rmi 命令。 以下是完整的流程 步骤1&#xff1a;停止容器 如容器正在运行需要停止正在运行的 Docker 容器&#xff0c;您可以使用 docker stop 命令。 以下是停止容器的步骤&#xff1a; 首先&#xff0c;使用 do…

二、PyTorch气温预测项目实战

一、数据集预处理 1&#xff0c;数据集介绍 训练数据集&#xff1a;temps.csv免费下载链接 数据集主要包括348条样本&#xff0c;共8个自变量&#xff0c;1个因变量 自变量因变量year&#xff1a;年actual&#xff1a;当天的真实最高温度month&#xff1a;月day&#xff1a;…

SCADA系统的三种架构

在工业自动化中&#xff0c;当需要使用各种设备时&#xff0c;有必要了解其中设计的架构。设备以各种方式相互通信 - 通过硬件或通信在现场和控制室之间共享数据。哪个环节进入哪个连接&#xff0c;是定义和解决所必需的&#xff0c;一旦我们了解了架构&#xff0c;那么我们就可…

血压计语音IC方案,低功耗语音芯片NV080C-SOP8

​近年来&#xff0c;随着智能化的发展&#xff0c;我们看到越来越多的医疗设备被智能化并应用到人们的生活中。这其中&#xff0c;血压计是其中之一&#xff0c;这是一种简单而普遍的医疗测量设备&#xff0c;用来测试人体的血压指数&#xff0c;它在生活中应用十分广泛。如今…

学Java其实不难,零基础小白如何快速学会Java?

去年的时候有个学弟刚跟好程序员说想转行&#xff0c;但是目前又比较迷茫&#xff0c;不知道该从事啥行业&#xff1f;跟小源说了下具体情况&#xff0c;小源也跟他分享了下相关的it行业规划&#xff0c;最后他学了一段时间的Java&#xff0c;成功找到一份Java的工作&#xff0…

Doo Prime 德璞资本:选择MT4外汇交易系统进行投资有哪些理由?

目前&#xff0c;只要你在做外汇交易&#xff0c;你就必须使用计算机或手机软件。目前网上有很多不同的交易软件&#xff0c;让投资者不知道如何选择。如果你正在从事或者听说过外汇保证金交易&#xff0c;那你可能多多少少有听过「MT4」这个词。但MT4到底是什么&#xff1f;为…

1-Single Thread

单线程执行模式 案例-1 背景 模拟3个人频繁地经过同一个只能容许一个人经过的门 。 &#xff08;模拟三个线程调用同一个对象的方法&#xff09; 当人通过门的时候&#xff0c;这个程序会在计数器中&#xff0c;递增通过的人数。另外&#xff0c; 还会记录通过的人的 “ 姓名与…

一篇文章带你了解Redis持久化机制(RDB、AOF)

目录 一、简介 什么是持久化&#xff1f; 为什么要持久化&#xff1f; 两种实现方式 二、RDB详解 2.1、介绍 2.2、save指令前后对比 2.3、save指令相关配置 一些设置 RDB快照条件 2.4、RDB第一种方式&#xff1a;手动save 2.5、RDB第二种方式&#xff1a;后台执行&…

加壳与脱壳,打造铁壁铜墙的Android应用防护境地

加壳和脱壳是什么&#xff1f; Android逆向加壳和脱壳是与Android应用程序安全相关的概念。 逆向加壳&#xff08;Reverse Engineering with Packing&#xff09;&#xff1a;逆向加壳是指在给定的Android应用程序中&#xff0c;通过添加一个或多个防护层或加密算法来增加应用…

AI2:仅凭开源数据,可达ChatGPT 83%表现

夕小瑶科技说 原创 作者 | Python ChatGPT强大的性能让人爱不释手&#xff0c;ChatGPT迟迟不开源让人恨得牙根痒痒。那仅通过开源数据&#xff0c;能够取得怎样的效果呢&#xff1f;近期&#xff0c;AI2的一篇论文显示&#xff0c;最好的65B规模的模型能够达到ChatGPT表现的8…

设计一个feed流系统

什么是feed流系统 移动互联网时代&#xff0c;Feed流产品是非常常见的&#xff0c;如朋友圈、微博、抖音等&#xff0c;除此之外&#xff0c;很多App的都会有一个模块&#xff0c;要么叫动态&#xff0c;要么叫消息广场&#xff0c;这些也是Feed流产品。只要大拇指不停地往下划…