首页 > 数据库 >2023-06-19:讲一讲Redis分布式锁的实现?

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

时间:2023-06-19 18:31:28浏览次数:40  
标签:加锁 06 19 redis Redis lock go id 客户端

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的互斥性来实现简单的分布式锁机制。

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

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

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

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

DEL lock_key

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

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

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

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

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

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

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

如何避免死锁?

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

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

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

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

但现在还是有问题:

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

  • SETNX 命令执行成功后,由于网络问题导致 EXPIRE 命令执行失败。
  • SETNX 命令执行成功后,Redis 异常宕机,导致 EXPIRE 命令没有机会执行。
  • SETNX 命令执行成功后,客户端异常崩溃,同样导致 EXPIRE 命令没有机会执行。

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

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

SET lock 1 EX 10 NX

2023-06-19:讲一讲Redis分布式锁的实现?_客户端_04

锁被别人释放怎么办?

上面的命令执行时,每个客户端在释放锁时,并没有进行严格的验证,存在释放别人锁的潜在风险。为了解决这个问题,可以在加锁时为每个客户端设置一个唯一的标识符(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
}

2023-06-19:讲一讲Redis分布式锁的实现?_客户端_05

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

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

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

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

分布式锁加入看门狗

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

为什么要使用守护线程:

2023-06-19:讲一讲Redis分布式锁的实现?_客户端_07

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
}

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

标签:加锁,06,19,redis,Redis,lock,go,id,客户端
From: https://blog.51cto.com/moonfdd/6516898

相关文章

  • 欧奈儿行业 RPS 排名,一图览全貌 2023-06-19
    自动复盘2023-06-19k线图是最好的老师,点击详情图可以看到行业20日RPS的排名,最底下子图是行业rps走势线跟踪板块总结:成交额超过100亿排名靠前,macd柱由绿转红成交量要大于均线有必要给每个行业加一个上级的归类,这样更能体现主流方向rps有时候比较滞后,但不少是欲......
  • 自学C语言 2023_6_19
    变量,常量:变量——能被改变的量常量——不能改变的量定义变量的方法:inta=150;floatb=45.5f;charc='w';变量   变量的分类:局部变量——在打括号内的变量为局部变量全局变量——在大括号外的是全局变量例:inta=100;intmain(){inta=10;printf("%d\n",a);ret......
  • BUUCTF:[ASIS 2019]Unicorn shop
    https://buuoj.cn/challenges#[ASIS%202019]Unicorn%20shop功能是一个购物商店,输入商品ID和价钱进行点击购买。源代码中提醒<metacharset="utf-8">很重要html使用的是UTF-8编码id和price都为空点击购买,返回报错及原因从中可以发现源代码是如何处理price的使用的是unicodedata......
  • BUUCTF:[BSidesSF2019]table-tennis
    https://buuoj.cn/challenges#[BSidesSF2019]table-tennisattachment.pcapng在ICMP包的尾部发现html代码,其中有打印base64信息拼接起来就是Q1RGe0p1c3RBUzBuZ0FiMHV0UDFuZ1Awbmd9>>>importbase64>>>base64.b64decode('Q1RGe0p1c3RBUzBuZ0FiMHV0UDFuZ1Awbmd9')b&......
  • BUUCTF:[GUET-CTF2019]soul sipse
    https://buuoj.cn/challenges#[GUET-CTF2019]soul%20sipseout.wav可用Steghide无密码分离出download.txtdownload.txthttps://share.weiyun.com/5wVTIN3下载得到GUET.png,修改为正确的PNG文件头保存得到正常的图片。如下\u0034\u0030\u0037\u0030\u000d\u000a\u0031\u0032\u0033\u0......
  • BUUCTF:[BSidesSF2019]diskimage
    https://buuoj.cn/challenges#[BSidesSF2019]diskimageattachment.pngzsteg-aattachment.png发现磁盘数据zsteg-e'b8,rgb,lsb,xy'attachment.png>data.dat分离出FAT格式的数据使用TestDisk对文件进行分析testdiskdata.dat[Proceed]回车[None]回车[Boot]回车[RebuildsBS]......
  • BUUCTF:[SWPU2019]伟大的侦探
    题目地址:https://buuoj.cn/challenges#[SWPU2019]%E4%BC%9F%E5%A4%A7%E7%9A%84%E4%BE%A6%E6%8E%A2密码.txt可解压,misc文件夹需要解压密码,将密码.txt使用010Editor打开,使用EBCDIC编码即可发现密码明文解压,misc文件夹内容如下:福尔摩斯小人密码对照得到:iloveholmesandwllmflag{ilov......
  • 2023-06-19 API `getMenuButtonBoundingClientRect` is not yet implemented
    前言:想使用该Api来获取设备导航栏高度,结果报错了:API`getMenuButtonBoundingClientRect`isnotyetimplemented尚未实现API`getMenuButtonBoundingClientRect`原因:该Api不支持在app端或者h5端使用。平台兼容如下: AppH5微信小程序支付宝小程序百度小程序抖音小程序飞书小......
  • 2023-06-19《计算方法》- 陈丽娟 - 方程的近似解法(注解)
    2023-06-19《计算方法》-陈丽娟-方程的近似解法(注解)Matlab计算方法二分法迭代法牛顿法前面介绍了求解方程的二分法、迭代法和牛顿迭代法,这里介绍弦截法,欸特金加速法。一、弦截法由于牛顿迭代法需要计算导数,而从上一章节我们看到导数的求解对数值稳定性会产生不良影响,为了......
  • LeetCode 周赛 350(2023/06/18)01 背包变型题
    本文已收录到AndroidFamily,技术和职场问题,请关注公众号[彭旭锐]和[BaguTreePro]知识星球提问。往期回顾:LeetCode单周赛第348场·数位DP模版学会了吗?T1.总行驶距离(Easy)标签:模拟T2.找出分区值(Medium)标签:排序T3.特别的排列(Medium)标签:图、状态压缩、......