当前位置: 首页 > news >正文

基于redis实现分布式锁

前言

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

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

业界流行的分布式锁实现,一般有这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]) 
elsereturn 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版本的锁,有兴趣的小伙伴,可以去了解一下哈~

相关文章:

基于redis实现分布式锁

前言 我们的系统都是分布式部署的&#xff0c;日常开发中&#xff0c;秒杀下单、抢购商品等等业务场景&#xff0c;为了防⽌库存超卖&#xff0c;都需要用到分布式锁。 分布式锁其实就是&#xff0c;控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或…...

C#开发的OpenRA动态加载插件DLL里的类实现

C#开发的OpenRA动态加载插件DLL里的类实现 由于这款游戏的设计是为了开源设计, 并且可以让不同个人或团体实现自己的游戏, 那么每个人实现的代码是不一样的,算法也是不一样的。 并且可能也拿不到代码一起编译生成一套运行的代码。 这时候,就要考虑使用动态加载类的功能。 意…...

网站代理是什么?有什么需要注意的?

如今&#xff0c;网站代理已经成为一种不可或缺的经营方式。无论是企业还是个人&#xff0c;都需要通过代理来获得更多的流量和市场份额。 一、网站代理的优势 网站代理的优势在于能够为您提供更加专业、周到的服务。这些优势包括&#xff1a;1.丰富的内容资源&#xff0c;能…...

动态库和静态库的区别

什么是库文件 一般来说&#xff0c;一个程序&#xff0c;通常都会包含目标文件和若干个库文件。经过汇编得到的目标文件再经过和库文件的链接&#xff0c;就能构成可执行文件。库文件像是一个代码仓库或代码组件的集合&#xff0c;为目标文件提供可直接使用的变量、函数、类等…...

C/C++路径去除前缀

在做一些日志输出的工作时&#xff0c;想要获取当前文件名&#xff0c;而不是冗长的文件路径。路径获取往往和各家os底层函数优化。C/C标准中定义了一些预处理宏&#xff0c;可以帮助我们获取文件路径。我们希望能够在编译期而不是在运行期做这个事情&#xff0c;避免额外的性能…...

Vue2之Vue-cli应用及组件基础认识

Vue2之Vue-cli应用及组件基础认识一、Vue-cli1、单页面应用程序2、vue-cli介绍3、安装和使用4、创建项目4.1 输入创建项目4.2 选择第三项&#xff0c;进行自主配置&#xff0c;按回车键即可4.3 选择自己需要的库4.4 选择Vue的版本4.5 选择CSS选择器4.6 选择Babel、ESLint、etc等…...

C 学习笔记 —— 声明、定义、初始化

文章目录声明定义初始化定义和初始化的区别静态变量初始化自动变量初始化声明 说明符表达式列表 int a; char j, k l;定义 一般的情况下&#xff0c;我们把建立空间的声明称之为定义&#xff0c;而把不需要建立存储空间的声明称之为声明。 int tern 1; //定义int main() {…...

机械狗控制算法

一. MIT Cheetah特点 1.驱动器 Cheetah 2采用了定制的本体感受驱动器设计&#xff0c;具有高冲击缓解、力控制和位置控制能力。这种设计使其能够自主跳过障碍物&#xff0c;并以6m/s的高速跳跃&#xff0c;但其运动范围有限&#xff0c;只能进行矢状面运动。 Cheetah 3采用高扭…...

向量与矩阵 导数和偏导数 特征值与特征向量 概率分布 期望方差 相关系数

文章目录向量与矩阵标量、向量、矩阵、张量向量范数和矩阵的范数导数和偏导数特征值和特征向量概率分布伯努利分布正态分布&#xff08;高斯分布&#xff09;指数分布期望、⽅差、协⽅差、相关系数期望方差协⽅差相关系数向量与矩阵 标量、向量、矩阵、张量 标量&#xff08;…...

记录--前端实现登录拼图验证

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 前言 不知各位朋友现在在web端进行登录的时候有没有注意一个变化&#xff0c;以前登录的时候是直接账号密码通过就可以直接登录&#xff0c;再后来图形验证码&#xff0c;数字结果运算验证&#xff0c…...

【Go语言基础】Go语言中的map集合详细使用(附带源码)

文章目录Go语言中的map集合1-1 定义1-2 map遍历1-3 map集合删除1-4 map是引用类型Go语言中的map集合 Go 语言提供了内置类型 map集合&#xff0c;它将一个值与一个键关联起来&#xff0c;可以使用相应的键检索值。 map是一种集合&#xff0c;可以像遍历数组或切片那样去遍历它…...

C++11 lambda

Lambda 介绍 Lambda 函数也叫匿名函数&#xff0c; 是C 11中新增的特性; 1. Lambda函数的好处 如果你的代码里面存在大量的小函数&#xff0c;而这些函数一般只被调用一次&#xff0c;那么将他们重构成 lambda 表达式。 Lambda函数使代码变得更加紧凑、更加结构化和更富有表现…...

【新】华为OD机试 - 分苹果(Python)

分苹果 题目 AB两个人把苹果分为两堆 A希望按照他的计算规则等分苹果 他的计算规则是按照二级制加法计算 并且不计算进位12+5=9(1100+0101=9), B的计算规则是十进制加法, 包括正常进位,B希望在满足A的情况下获取苹果重量最多 输入苹果的数量和每个苹果重量 输出满足A的情况下…...

Python 模块

Python 模块(Module)&#xff0c;是一个 Python 文件&#xff0c;以 .py 结尾&#xff0c;包含了 Python 对象定义和Python语句。 模块让你能够有逻辑地组织你的 Python 代码段。 把相关的代码分配到一个模块里能让你的代码更好用&#xff0c;更易懂。 模块能定义函数&#…...

gdb调试功能从零到会(Linux详解)

目录 &#x1f440; 1.安装gdb &#x1f440;2.判断是否安装成功 &#x1f440;3.改成debug方式发布。 &#x1f440; 4.gdb功能简介 前言 gdb是Linux 下功能全面的调试工具。gdb支持断点、单步执行、打印变量、观察变量、查看寄存器、查看堆栈等调试手段。在Linux环境软件…...

【C语言学习笔记】:数组、指针相关面试题

无特殊说明情况下&#xff0c;下面所有题s目都是linux下的32位C程序。 「1、计算以下sizeof的值。」 char str1[] {a, b, c, d, e}; char str2[] "abcde";char *ptr "abcde";char book[][80]{"计算机应用基础","C语言","C程…...

go语言环境配置 项目启动

一 安装go语言 go语言各个版本之间兼容性比较差。所以可能你需要安装固定的版本 1 安装最新版的go brew install go2 查看go可以安装的版本 brew search go3 安装指定版本的go brew install go1.134 查看安装的go语言的版本 go version5 查看go的安装路径 which go || w…...

Springboot 使用插件 自动生成Mock单元测试 Squaretest

缘起 很多公司对分支单测覆盖率会有一定的要求&#xff0c;比如 单测覆盖率要达到 60% 或者 80%才可以发布。 有时候工期相对紧张&#xff0c;就优先开发功能&#xff0c;测试功能&#xff0c;然后再去补单元测试。 但是编写单元测试又比较浪费时间&#xff0c;有没有能够很大…...

「JVM 执行引擎」栈架构的字节码的解释执行引擎

JVM 执行引擎在执行 Java 代码时有解释执行&#xff08;通过解释器执行&#xff09;和编译执行&#xff08;通过即时编译器产生本地代码执行&#xff09;两种选择&#xff1b; HotSpot 实际的实现中&#xff0c;模版解释器工作时&#xff0c;并不是按照概念模型中进行机械式计…...

SSM项目-商城后台管理系统

SSM项目-商城后台管理系统开发说明开发环境项目界面演示项目功能具体的技术指标开发过程1、搭建SSM框架1.1、建库建表1.2、新建Maven工程1.3、配置pom.xml1.4、目录结构1.5、jdbc.properties1.6、mybatis-config.xml1.7 两个Spring的配置文件applicationContext_dao.xmlapplica…...

LeetCode - 394. 字符串解码

题目 394. 字符串解码 - 力扣&#xff08;LeetCode&#xff09; 思路 使用两个栈&#xff1a;一个存储重复次数&#xff0c;一个存储字符串 遍历输入字符串&#xff1a; 数字处理&#xff1a;遇到数字时&#xff0c;累积计算重复次数左括号处理&#xff1a;保存当前状态&a…...

pam_env.so模块配置解析

在PAM&#xff08;Pluggable Authentication Modules&#xff09;配置中&#xff0c; /etc/pam.d/su 文件相关配置含义如下&#xff1a; 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块&#xff0c;负责验证用户身份&am…...

Nginx server_name 配置说明

Nginx 是一个高性能的反向代理和负载均衡服务器&#xff0c;其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机&#xff08;Virtual Host&#xff09;。 1. 简介 Nginx 使用 server_name 指令来确定…...

AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机

这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机&#xff0c;因为在使用过程中发现 Airsim 对外部监控相机的描述模糊&#xff0c;而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置&#xff0c;最后在源码示例中找到了&#xff0c;所以感…...

Modbus RTU与Modbus TCP详解指南

目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...

Spring Boot + MyBatis 集成支付宝支付流程

Spring Boot MyBatis 集成支付宝支付流程 核心流程 商户系统生成订单调用支付宝创建预支付订单用户跳转支付宝完成支付支付宝异步通知支付结果商户处理支付结果更新订单状态支付宝同步跳转回商户页面 代码实现示例&#xff08;电脑网站支付&#xff09; 1. 添加依赖 <!…...

快速排序算法改进:随机快排-荷兰国旗划分详解

随机快速排序-荷兰国旗划分算法详解 一、基础知识回顾1.1 快速排序简介1.2 荷兰国旗问题 二、随机快排 - 荷兰国旗划分原理2.1 随机化枢轴选择2.2 荷兰国旗划分过程2.3 结合随机快排与荷兰国旗划分 三、代码实现3.1 Python实现3.2 Java实现3.3 C实现 四、性能分析4.1 时间复杂度…...

Vue3中的computer和watch

computed的写法 在页面中 <div>{{ calcNumber }}</div>script中 写法1 常用 import { computed, ref } from vue; let price ref(100);const priceAdd () > { //函数方法 price 1price.value ; }//计算属性 let calcNumber computed(() > {return ${p…...

基于单片机的宠物屋智能系统设计与实现(论文+源码)

本设计基于单片机的宠物屋智能系统核心是实现对宠物生活环境及状态的智能管理。系统以单片机为中枢&#xff0c;连接红外测温传感器&#xff0c;可实时精准捕捉宠物体温变化&#xff0c;以便及时发现健康异常&#xff1b;水位检测传感器时刻监测饮用水余量&#xff0c;防止宠物…...

深入浅出WebGL:在浏览器中解锁3D世界的魔法钥匙

WebGL&#xff1a;在浏览器中解锁3D世界的魔法钥匙 引言&#xff1a;网页的边界正在消失 在数字化浪潮的推动下&#xff0c;网页早已不再是静态信息的展示窗口。如今&#xff0c;我们可以在浏览器中体验逼真的3D游戏、交互式数据可视化、虚拟实验室&#xff0c;甚至沉浸式的V…...