redis实现相关分布式锁

为什么需要分布式锁
我们知道,当多个线程并发操作某个对象时,可以通过synchronized来保证同一时刻只能有一个线程获取到对象锁进而处理synchronized关键字修饰的代码块或方法。既然已经有了synchronized锁,为什么这里又要引入分布式锁呢?

因为现在的系统基本都是分布式部署的,一个应用会被部署到多台服务器上,synchronized只能控制当前服务器自身的线程安全,并不能跨服务器控制并发安全。比如下图,同一时刻有4个线程新增同一件商品,其中两个线程由服务器A处理,另外两个线程由服务器B处理,那么最后的结果就是两台服务器各执行了一次新增动作。这显然不符合预期。
在这里插入图片描述

而本篇文章要介绍的分布式锁就是为了解决这种问题的。

什么是分布式锁
分布式锁,就是控制分布式系统中不同进程共同访问同一共享资源的一种锁的实现。

所谓当局者迷,旁观者清,先举个生活中的例子,就拿高铁举例,每辆高铁都有自己的运行路线,但这些路线可能会与其他高铁的路线重叠,如果只让高铁内部的司机操控路线,那就可能出现撞车事故,因为司机不知道其他高铁的运行路线是什么。所以,中控室就发挥作用了,中控室会监控每辆高铁,高铁在什么时间走什么样的路线全部由中控室指挥。

分布式锁就是基于这种思想实现的,它需要在我们分布式应用的外面使用一个第三方组件(可以是数据库、Redis、Zookeeper等)进行全局锁的监控,由这个组件决定什么时候加锁,什么时候释放锁。
在这里插入图片描述

Redis如何实现分布式锁
在聊Redis如何实现分布式锁之前,我们要先聊一下redis的一个命令:setnx key value。我们知道,Redis设置一个key最常用的命令是:set key value,该命令不管key是否存在,都会将key的值设置成value,并返回成功:
在这里插入图片描述

setnx key value 也是设置key的值为value,不过,它会先判断key是否已经存在,如果key不存在,那么就设置key的值为value,并返回1;如果key已经存在,则不更新key的值,直接返回0:
**加粗样式
**

● 最简单的版本:setnx key value

基于setnx命令的特性,我们就可以实现一个最简单的分布式锁了。我们通过向Redis发送 setnx 命令,然后判断Redis返回的结果是否为1,结果是1就表示setnx成功了,那本次就获得锁了,可以继续执行业务逻辑;如果结果是0,则表示setnx失败了,那本次就没有获取到锁,可以通过循环的方式一直尝试获取锁,直至其他客户端释放了锁(delete掉key)后,就可以正常执行setnx命令获取到锁。流程如下:
在这里插入图片描述

这种方式虽然实现了分布式锁的功能,但有一个很明显的问题:没有给key设置过期时间,万一程序在发送delete命令释放锁之前宕机了,那么这个key就会永久的存储在Redis中了,其他客户端也永远获取不到这把锁了。

● 升级版本:设置key的过期时间

针对上面的问题,我们可以基于setnx key value的基础上,同时给key设置一个过期时间。Redis已经提供了这样的命令:set key value ex seconds nx。其中,ex seconds 表示给key设置过期时间,单位为秒,nx 表示该set命令具备setnx的特性。效果如下:
在这里插入图片描述

我们设置name的过期时间为60秒,60秒内执行该set命令时,会直接返回nil。60秒后,我们再执行set命令,可以执行成功,效果如下:
在这里插入图片描述

基于这个特性,升级后的分布式锁流程如下:
在这里插入图片描述

这种方式虽然解决了一些问题,但却引来了另外一个问题:存在锁误删的情况,也就是把别人加的锁释放了。例如,client1获得锁之后开始执行业务处理,但业务处理耗时较长,超过了锁的过期时间,导致业务处理还没结束时,锁却过期自动删除了(相当于属于client1的锁被释放了),此时,client2就会获取到这把锁,然后执行自己的业务处理,也就在此时,client1的业务处理结束了,然后向Redis发送了delete key的命令来释放锁,Redis接收到命令后,就直接将key删掉了,但此时这个key是属于client2的,所以,相当于client1把client2的锁给释放掉了:
在这里插入图片描述

● 二次升级版本:value使用唯一值,删除锁时判断value是否当前线程的

要解决上面的问题,最省事的做法就是把锁的过期时间设置长一点,要远大于业务处理时间,但这样就会严重影响系统的性能,假如一台服务器在释放锁之前宕机了,而锁的超时时间设置了一个小时,那么在这一个小时内,其他线程访问这个服务时就一直阻塞在那里。所以,一般不推荐使用这种方式。

另一种解决方法就是在set key value ex seconds nx时,把value设置成一个唯一值,每个线程的value都不一样,在删除key之前,先通过get key命令得到value,然后判断value是否是自己线程生成的,如果是,则删除掉key释放锁,如果不是,则不删除key。正常流程如下:
在这里插入图片描述

当业务处理还没结束的时候,key自动过期了,也可以正常释放自己的锁,不影响其他线程:

在这里插入图片描述

二次升级后的方案看起来似乎已经没什么问题了,但其实不然。仔细分析流程后我们发现,判断锁是否属于当前线程和释放锁两个步骤并不是原子操作。正常来说,如果线程1通过get操作从Redis中得到的value是123,那么就会执行删除锁的操作,但假如在执行删除锁的动作之前,系统卡顿了几秒钟,恰好在这几秒钟内,key自动过期了,线程2就顺利获取到锁开始执行自己的逻辑了,此时,线程1卡顿恢复了,开始继续执行删除锁的动作,那么此时删除的还是线程2的锁。
在这里插入图片描述

● 终极版本:Lua脚本

针对上述Redis原始命令无法满足部分业务原子性操作的问题,Redis提供了Lua脚本的支持。Lua脚本是一种轻量小巧的脚本语言,它支持原子性操作,Redis会将整个Lua脚本作为一个整体执行,中间不会被其他请求插入,因此Redis执行Lua脚本是一个原子操作。

在上面的流程中,我们把get key value、判断value是否属于当前线程、删除锁这三步写到Lua脚本中,使它们变成一个整体交个Redis执行,改造后流程如下:
在这里插入图片描述

这样改造之后,就解决了释放锁时取值、判断值、删除锁等多个步骤无法保证原子操作的问题了。关于Lua脚本的语法可以自行学习,并不复杂,很简单,这里就不做过多讲述。

既然Lua脚本可以在释放锁时使用,那肯定也能在加锁时使用,而且一般情况下,推荐使用Lua脚本,因为在使用上面set key value ex seconds nx命令加锁时,并不能做到重入锁的效果,也就是当一个线程获取到锁后,在没有释放这把锁之前,当前线程自己也无法再获得这把锁,这显然会影响系统的性能。使用Lua脚本就可以解决这个问题,我们可以在Lua脚本中先判断锁(key)是否存在,如果存在则再判断持有这把锁的线程是否是当前线程,如果不是则加锁失败,否则当前线程再次持有这把锁,并把锁的重入次数+1。在释放锁时,也是先判断持有锁的线程是否是当前线程,如果是则将锁的重入次数-1,直至重入次数减至0,即可删除该锁(key)。
在这里插入图片描述

实际项目开发中,其实基本不用自己写上面这些分布式锁的实现逻辑,而是使用一些很成熟的第三方工具,当下比较流行的就是Redisson,它既提供了Redis的基本命令的封装,也提供了Redis分布式锁的封装,使用非常简单,只需直接调用相应方法即可。但工具虽然好用,底层原理还是要理解的,这就是本篇文章的目的。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Spring Boot】Spring Boot的系统配置 — 系统配置文件

系统配置文件 Spring Boot的系统配置文件,包括application.properties和application.yml配置文件的使用以及YML和Properties配置文件有什么区别,最后介绍如何更改Spring Boot的启动图案。 1.application.properties Spring Boot支持两种不同格式的配置…

Oracle初级

目录 概念 数据库分类 Oracle 存储结构 安装成功 ​编辑 创建用户和表空间 以超级管理员身份登录 创建表空间 创建用户 给用户授权 查询测试 概念 数据库(database): 物理操作系统文件或磁盘的集合。简单来说数据库的意思是数据的集合。 DBM…

Leetcode每日一题:931. 下降路径最小和(2023.7.13 C++)

目录 931. 下降路径最小和 题目描述: 实现代码与解析: 动态规划 原理思路: 931. 下降路径最小和 题目描述: 给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。 下降…

前端Vue自定义商品订单星级评分 爱心评分组件

随着技术的发展,开发的复杂度也越来越高,传统开发方式将一个系统做成了整块应用,经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改,造成牵一发而动全身。 通过组件化开发,可以有效实现…

ModaHub魔搭社区:AI原生云向量数据库Zilliz Cloud与 LangChain 集成搭建智能文档问答系统

目录 准备工作 主要参数 准备数据 开始提问 本文将演示如何使用 Zilliz Cloud 和 LangChain 搭建基于大语言模型(LLM)的问答系统。在本例中,我们将使用一个 1 CU 的 Cluster,还将使用 OpenAI 的 Embedding API 来获取指定文本的向量表示。现在就让我们开始吧。 准备工作…

【Ajax】Express 服务端框架

因为Ajax需要向服务端发送请求。Express框架比较简单,内容使用起来比较少,借助一个基本功能就可以了。 Express 基于 Node.js 平台,快速、开放、极简的 Web 开发框架 所以需要安装一下node.js 检查命名:node -v 安装 打开项目最外…

GTK列表显示文本和图片

使用GtkTreeView控件显示包含文本和图片的列表,GtkTreeView/GtkListStore或者GtkTreeView/GtkTreeModel使用的是MVC设计理念。 关于MVC: M层: model 数据模型层(处理数据的增删改查) 提供数据 V层: Views 视图层 (数据展示) 渲染数据 C层: controller 控制层(处理业…

window电脑修复网络不能正常

问题描述 问题的起点是我打开了OpenAPI公司的GPT,在回答的过程中响应很慢,然后自己开始尝试切换连接的服务器(这里使用到了网络代理),最后自己做了一个操作是 代理软件的这个菜单里面的增强模式选项,结果…

Flink基本原理剖析讲解

1.Flink简介 Flink是一个批处理和流处理结合的统一计算框架,其核心是一个提供了数据分发以及并行化计算的流数据处理引擎。它的最大亮点是流处理,是业界最顶级的开源流处理引擎。 Flink最适合的应用场景是低时延的数据处理(Data Processing…

Proton 推出开源密码管理器,兼身份管理器

Proton 是由来自欧洲核研究组织 (CERN) 的科学家于 2014 年在瑞士日内瓦创立的一家公司,其最知名的应该就是电子邮件服务 Proton Mail,主打端到端加密、安全和隐私保护。Proton 由科学家领导,其中包括万维网的发明者 Tim Berners-Lee。 该公…

git HEAD detached from

git HEAD detached from 解决,checkout切换分支即可,比如切换到master分支: git checkout master git gerrit code review提交代码HEAD:resf/for/_res/for的提交格式_zhangphil的博客-CSDN博客git gerrit code review提交代码HEAD:resf/for/如…

Flink实时任务性能调优

前言 通常我们在开发完Flink任务提交运行后,需要对任务的参数进行一些调整,通常需要调整的情况是任务消费速度跟不上数据写入速度,从而导致实时任务出现反压、内存GC频繁(FullGC)频繁、内存溢出导致TaskManager被Kill…