基于redis实现分布式锁

news2024/11/24 2:40:09

前言

我们的系统都是分布式部署的,日常开发中,秒杀下单、抢购商品等等业务场景,为了防⽌库存超卖,都需要用到分布式锁。

分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。

业界流行的分布式锁实现,一般有这3种方式:

  • 基于数据库实现的分布式锁。
  • 基于Redis实现的分布式锁。
  • 基于Zookeeper实现的分布式锁。
    那下面我们就来聊聊基于数据库实现的分布式锁。

基于Redis实现的分布式锁

Redis分布式锁一般有以下这几种实现方式:

  • setnx + expire。
  • setnx + value值是过期时间。
  • set的扩展命令(set ex px nx)。
  • set ex px nx + 校验唯一随机值,再删除。
  • Redisson。
  • Redisson + RedLock。

setnx + expire

聊到Redis分布式锁,很多小伙伴反手就是setnx + expire,如下:

if(jedis.setnx(key,lock_value) == 1{ //setnx加锁
    expire(key,100; //设置过期时间
    try {
        do something  //业务处理
    }catch(){
    }
  finally {
       jedis.del(key); //释放锁
    }
}

这段代码是可以加锁成功,但是你有没有发现问题,加锁操作和设置超时时间是分开的。假设在执行完setnx加锁后,正要执行expire设置过期时间时,进程crash掉或者要重启维护了,那这个锁就长生不老了,别的线程永远获取不到锁啦,所以分布式锁不能这么实现!

long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);

// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key, expiresStr) == 1) {
        return true;
} 
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key);

// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

     // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
    String oldValueStr = jedis.getSet(key, expiresStr);
    
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
         return true;
    }
}

//其他情况,均返回加锁失败
return false;
}

日常开发中,有些小伙伴就是这么实现分布式锁的,但是会有这些缺点:

  • 过期时间是客户端自己生成的,分布式环境下,每个客户端的时间必须同步。
  • 没有保存持有者的唯一标识,可能被别的客户端释放/解锁。
  • 锁过期的时候,并发多个客户端同时请求过来,都执行了jedis.getSet(),最终只能有一个客户端加锁* 成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。

set的扩展命令(set ex px nx)

这个命令的几个参数分别表示什么意思呢?跟大家复习一下:

SET key value [EX seconds] [PX milliseconds] [NX|XX]

EX second :设置键的过期时间为second秒。
PX millisecond :设置键的过期时间为millisecond毫秒。
NX :只在键不存在时,才对键进行设置操作。
XX :只在键已经存在时,才对键进行设置操作。

if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1{ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       jedis.del(key); //释放锁
    }
}

这个方案可能存在这样的问题:

  • 锁过期释放了,业务还没执行完。
  • 锁被别的线程误删。

有些伙伴可能会有个疑问,就是锁为什么会被别的线程误删呢?假设并发多线程场景下,线程A获得了锁,但是它没释放锁的话,线程B是获取不到锁的,所以按道理它是执行不到加锁下面的代码滴,怎么会导致锁被别的线程误删呢?

假设线程A和B,都想用key加锁,最后A抢到锁加锁成功,但是由于执行业务逻辑的耗时很长,超过了设置的超时时间100s。这时候,Redis就自动释放了key锁。这时候线程B就可以加锁成功了,接下啦,它也执行业务逻辑处理。假设碰巧这时候,A执行完自己的业务逻辑,它就去释放锁,但是它就把B的锁给释放了。

set ex px nx + 校验唯一随机值,再删除

为了解决锁被别的线程误删问题。可以在set ex px nx的基础上,加上个校验的唯一随机值,如下:

(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1{ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key))) {
          jedis.del(key); //释放锁
        }
    }
}

在这里,判断当前线程加的锁和释放锁不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
一般可以用lua脚本来包一下。lua脚本如下:

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;

这种方式比较不错了,一般情况下,已经可以使用这种实现方式。但是还是存在:锁过期释放了,业务还没执行完的问题。

Redisson

对于可能存在锁过期释放,业务没执行完的问题。我们可以稍微把锁过期时间设置长一些,大于正常业务处理时间就好啦。如果你觉得不是很稳,还可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

当前开源框架Redisson解决了这个问题。可以看下Redisson底层原理图:
在这里插入图片描述
只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用watch dog解决了锁过期释放,业务没执行完问题。

Redisson + RedLock

前面六种方案都只是基于Redis单机版的分布式锁讨论,还不是很完美。因为Redis一般都是集群部署的:
在这里插入图片描述
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以顺理成章获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

为了解决这个问题,Redis作者antirez提出一种高级的分布式锁算法:Redlock。它的核心思想是这样的:

部署多个Redis master,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。
在这里插入图片描述
RedLock的实现步骤:

  • 获取当前时间,以毫秒为单位。
  • 按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
  • 客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)。
  • 如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
  • 如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。

简化下步骤就是:

  • 按顺序向5个master节点请求加锁。
  • 根据设置的超时时间来判断,是不是要跳过该master节点。
  • 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
  • 如果获取锁失败,解锁!

Redisson实现了redLock版本的锁,有兴趣的小伙伴,可以去了解一下哈~

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

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

相关文章

ubuntu重启、关机命令

// // // //之前用linux系统&#xff0c; 一键解决也是可以的&#xff0c;反正我每次用命令&#xff08;泪目…&#xff09;&#xff0c;中间崩了好几次&#xff0c;换回win&#xff0c;此篇也做记录 // // // 重启命令 以下所有命令在root根目录下输入&#xff08;普通用户&…

SQL Server 数据批量导出处理

在实际项目环境中&#xff0c;有时会遇到需要将大量数据&#xff08;这里所指百万级别以上的数据量&#xff09;从一台服务器迁移到另外一台数据库服务器的情况。SQL Server有很多方式可以进行数据迁移&#xff1a;备份还原、导入/导出数据、生成脚本&#xff08;包含数据&…

解决console.log打印不出深度链表问题

解决console.log打印不出深度链表问题相信大家在写算法题的时候会遇到这样一个问题&#xff0c;写了一个链表的数据结构&#xff0c;在append几个数据之后&#xff0c;想console.log打印以下看看append正不正确&#xff0c;但是&#xff0c;consolo.log出来成这个样子&#xff…

【算法】差分

作者&#xff1a;指针不指南吗 专栏&#xff1a;算法篇 &#x1f43e;合理规划时间与精力&#x1f43e; 1.什么是差分&#xff1f; 与前缀和是反函数 原数组a a1 , a2 , a3 , a4 , a5 , a6 , a7 构造数组b a1b1; a2b1b2; a3b1b2b3; … aib1b2b3…bi; 构造一个b数组使得&#…

【Linux】信号量

&#x1f387;Linux&#xff1a; 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看见坚持…

Java基础:面向对象进阶

1.static 1.static概念 工具类 2.static内存图 静态变量是随着类的加载而加载的,优于对象出现 3.static的注意事项 1.静态方法中,只能访问静态 : 因为非静态方法一般会传入调用方法的对象的地址this(一般是虚拟机自动调用,不需要手动传入, 如student.study()).但是由于静态方…

MapStruct使用指北

mapstruct官方文档链接&#xff0c;点击跳转 mapstruct是什么&#xff1f; MapStruct 是一个代码生成器&#xff0c;它基于约定优于配置方法极大地简化了 Java bean 类型之间映射的实现。 生成的映射代码使用简单的方法调用&#xff0c;因此速度快、类型安全且易于理解。 为…

介绍一款HCIA、HCIP、HCIE的刷题软件

华为认证考试分为三个等级&#xff0c;分别为工程师HCIA、高级工程师HCIP、专家HCIE&#xff0c;等级越高&#xff0c;考试难度越大。 本篇带大家详细了解华为数通题库刷题工具的详细操作步骤。 操作须知&#xff1a;本款刷题工具为一款刷题小程序&#xff0c;无需安装即可在线…

vue2源码之生命周期篇

vue2源码之生命周期篇vue2源码之生命周期篇生命周期流程图初始化阶段&#xff08;new Vue&#xff09;vue2源码之生命周期篇 生命周期流程图 从图中可以看到&#xff0c;Vue实例的生命周期大致可分为4个阶段&#xff1a; 初始化阶段&#xff1a;为Vue实例上初始化一些属性&am…

YOLOv8 目标检测 | 自定义数据集

本文介绍了使用用于目标检测的自定义数据训练 YOLOv8 模型。我正在使用来自 kaggle 的 yolo 格式的“Face Mask Dataset”&#xff0c;数据集链接如下&#xff1a;https://www.kaggle.com/datasets/maalialharbi/face-mask-dataset?resourcedownloadYOLOv8 是目前最先进的 YOL…

一【 mybatis的工作流程】

目录一.mybatis执行流程二.使用工具类简化项目&#xff08;反射的体现&#xff09;2.1 Sqlsession工厂对像2.2 工具类&#xff08;可直接放在工具类使用&#xff09;一.mybatis执行流程 1.1 读取主配置文件mybatis-config.xml&#xff0c;获得运行环境和数据库连接。 1.2 加载映…

35.网络结构与模型压缩、加速-2

35.1 Depthwise separable convolution Depthwise separable convolution是由depthwise conv和pointwise conv构成depthwise conv(DW)有效减少参数数量并提升运算速度 但是由于每个feature map只被一个卷积核卷积,因此经过DW输出的feature map不能只包含输入特征图的全部信息,…

【C/C++基础练习题】复习题三

C复习题知识点记录&#xff1a; 在定义结构体类型时&#xff0c;不可以为成员设置默认值。 在公用一个共用体变量时。系统为其分配存储空间的原则是按成员中占内存空间最大者分配 a ,La, "a", L"a" 字符 长字符 字符串 长字符串 布尔类型只有两个值 fal…

蓝桥杯C/C++VIP试题每日一练之2n皇后问题

💛作者主页:静Yu 🧡简介:CSDN全栈优质创作者、华为云享专家、阿里云社区博客专家,前端知识交流社区创建者 💛社区地址:前端知识交流社区 🧡博主的个人博客:静Yu的个人博客 🧡博主的个人笔记本:前端面试题 个人笔记本只记录前端领域的面试题目,项目总结,面试技…

C语言(文件,流,键盘输入和输出以及文件结尾)

目录 一.文件 二.流 三.输入和输出 二.文件结尾 一.检测方法 二.不同的操作系统&#xff0c;文件结束方式 3.使用scanf检测EOF 一.文件 文件&#xff08;file&#xff09;是存储器中存储信息的区域。通常&#xff0c;文件都保存在某种永久存储器中。文件对于计算机系统相当重要…

【C++之容器适配器】栈和队列模拟实现

目录前言一、栈(stack)1. 基本代码结构2. 简介3. 成员类型4. 成员函数1. 构造函数2. empty()3. size()4. top()5. push()6.pop()7. 综合测试实现的stack的所有函数接口二、队列(queue)1. 基本代码结构2. 队列的简介3. 成员类型4. 成员函数1. 构造函数2. empty()3. size()4. fro…

leaflet 上传KML文件,导出geojson文件(065)

第065个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中本地上传kml文件,利用解析此kml文件,在地图上显示图形。点击导出geojson,将下载为geojson文件。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代…

ChatGPT 怎么用最新详细教程-新手小白一看就会

ChatGPT 以其强大的信息整合和对话能力惊艳了全球&#xff0c;在自然语言处理上面表现出了惊人的能力。这么强大的工具我们都想体验一下&#xff0c;那么 ChatGPT 怎么用呢&#xff1f;本文将给你逐步详细介绍。使用 ChatGPT 主要有4步&#xff1a;注册 ChatGPT 账号通过短信接…

vue 开发环境 卸载node 版本 切换新的 node 版本 mac电脑

注意&#xff1a;操作的机器当前是mac&#xff0c;先卸载&#xff0c;再安装 1.查看现有 node 版本 node -v2.卸载现有 node 版本&#xff0c; 1.卸载从node官网下载pkg安装的node sudo rm -rf /usr/local/{bin/{node,npm},lib/node_modules/npm,lib/node,share/man/*/node…

地址空间 (Address Space)

文章目录&#xff1a;程序地址空间进程地址空间程序地址空间 什么是地址空间&#xff1a;地址空间是内存中可供程序或进程使用的有效地址的范围。也就是说&#xff0c;它是程序或进程可以访问的内存。内存可以是物理的、也可以是虚拟的&#xff0c;用于执行指令和存储数据。 …