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

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

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

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

DEL lock_key

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

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

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

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

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

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

如何避免死锁?

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

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

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

但现在还是有问题:

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

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

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

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

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

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

SET lock 1 EX 10 NX

锁被别人释放怎么办?

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

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

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

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

分布式锁加入看门狗

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

为什么要使用守护线程:

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分布式锁的实现?的更多相关文章

  1. SpringBoot集成Redis分布式锁以及Redis缓存

    https://blog.csdn.net/qq_26525215/article/details/79182687 集成Redis 首先在pom.xml中加入需要的redis依赖和缓存依赖 < ...

  2. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

  3. 分布式-技术专区-Redis分布式锁实现-第一步

    承接前面一篇Redis分布式锁的原理介绍 https://www.cnblogs.com/liboware/p/11921759.html 我们针对于实现方案进行接下来上篇进行重新的规划和定义以及完善 ...

  4. redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  5. Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  6. Redis分布式锁---完美实现

    这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...

  7. redis分布式锁实践

    分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于jvm的锁无法满足多实例中锁的需求,本篇将讲下redis如何通过Lua脚本实现分布式锁,不同于网上的redission,完全是手动实现的 我 ...

  8. 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

  9. Redlock(redis分布式锁)原理分析

    Redlock:全名叫做 Redis Distributed Lock;即使用redis实现的分布式锁: 使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击) ...

  10. Redis分布式锁的实现

    前段时间,我在的项目组准备做一个类似美团外卖的拼手气红包[第X个领取的人红包最大],基本功能实现后,就要考虑这一操作在短时间内多个用户争抢同一资源的并发问题了,类似于很多应用如淘宝.京东的秒杀活动场景 ...

随机推荐

  1. 提交docker镜像

    docker commit -m="提交的描述信息" -a="作者" 容器id 目标镜像名:[TAG]

  2. Prometheus+Grafana监控系统

    Prometheus vs Zabbix Zabbix的客户端更多是只做上报的事情,push模式.而Prometheus则是客户端本地也会存储监控数据,服务端定时来拉取想要的数据. Zabbix的客户 ...

  3. Python最详细的Excel操作方式,

    为 什么要学会使用Excel在机器学习模型的建立中,不得不借助于Excel强大的统计分析能力,这个时候就牵涉到了如何将python直接处理得到的数据存入excel表格当中以及获取表格的数据进行分析,简 ...

  4. python之zipfile应用

    zipfile Python 中 zipfile 模块提供了对 zip 压缩文件的一系列操作. 1 f=zipfile.ZipFile("test.zip",mode=" ...

  5. .NET Core MongoDB数据仓储和工作单元模式封装

    前言 上一章我们把系统所需要的MongoDB集合设计好了,这一章我们的主要任务是使用.NET Core应用程序连接MongoDB并且封装MongoDB数据仓储和工作单元模式,因为本章内容涵盖的有点多关 ...

  6. SpringBoot2 简明教程

    1.环境配置: ●Java 8 & 兼容java14 .●Maven 3.3+●idea 2019.1.2 maven的settings.xml配置 <mirrors> <m ...

  7. 人工智能AI图像风格迁移(StyleTransfer),基于双层ControlNet(Python3.10)

    图像风格迁移(Style Transfer)是一种计算机视觉技术,旨在将一幅图像的风格应用到另一幅图像上,从而生成一幅新图像,该新图像结合了两幅原始图像的特点,目的是达到一种风格化叠加的效果,本次我们 ...

  8. VUEX(状态管理)之憨憨篇

    1.导入vuex包 import vuex from 'vuex' 2.注册vuex到vue中 vue.use(vuex) 3.new vuex.store() 得到一个数据存储对象 var stor ...

  9. 【Docker】镜像制作和管理

    一.Docker镜像说明 二.基于容器通过 docker commit 手动制作镜像 1.基于容器手动制作镜像步骤 1.下载官方系统镜像 2.基于官方基础镜像启动容器,并进入容器 3.在容器中进行配置 ...

  10. 音视频八股文(4)--ffmpeg常见命令(3)

    17 FFmpeg滤镜 17.1 filter的分类 按照处理数据的类型,通常多媒体的filter分为: ● 音频filter ● 视频filter ● 字幕filter 另一种按照处于编解码器的位置 ...