当前位置: 首页 > 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…...

Python爬虫实战:研究MechanicalSoup库相关技术

一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...

19c补丁后oracle属主变化,导致不能识别磁盘组

补丁后服务器重启&#xff0c;数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后&#xff0c;存在与用户组权限相关的问题。具体表现为&#xff0c;Oracle 实例的运行用户&#xff08;oracle&#xff09;和集…...

java 实现excel文件转pdf | 无水印 | 无限制

文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)

目录 1.TCP的连接管理机制&#xff08;1&#xff09;三次握手①握手过程②对握手过程的理解 &#xff08;2&#xff09;四次挥手&#xff08;3&#xff09;握手和挥手的触发&#xff08;4&#xff09;状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍

文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结&#xff1a; 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析&#xff1a; 实际业务去理解体会统一注…...

ElasticSearch搜索引擎之倒排索引及其底层算法

文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...

AI编程--插件对比分析:CodeRider、GitHub Copilot及其他

AI编程插件对比分析&#xff1a;CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展&#xff0c;AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者&#xff0c;分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

MySQL中【正则表达式】用法

MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现&#xff08;两者等价&#xff09;&#xff0c;用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例&#xff1a; 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)

引言 在人工智能飞速发展的今天&#xff0c;大语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为技术领域的焦点。从智能写作到代码生成&#xff0c;LLM 的应用场景不断扩展&#xff0c;深刻改变了我们的工作和生活方式。然而&#xff0c;理解这些模型的内部…...

苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会

在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...