【从零开始学习Redis | 第六篇】爆改Setnx实现分布式锁

news2024/9/29 5:23:54

前言:

      在Java后端业务中, 如果我们开启了均衡负载模式,也就是多台服务器处理前端的请求,就会产生一个问题:多台服务器就会有多个JVM,多个JVM就会导致服务器集群下的并发问题。我们在这里提出的解决思路是把锁交给Redis来实现,因为Redis是单线程的。而最基础的Redis解决集群模式下的并发问题的核心解决方案是使用Setnx构造分布式锁,下文来让我们详细的看一下过程。

目录

前言:

核心思路: 

具体业务逻辑:

业务问题解决思路

1.选择加锁问题:

2.Redis分布式锁的误删问题:

3,如何保证删除锁代码的原子性?

业务杂项知识点:

1.Spring mvc中的事务失效引起的并发问题:

2.包装类与基本数据类型的差异:

总结:


 

核心思路: 

 其实整个爆改过程的思路都很清楚,我们先来解释一下SETNX的作用:

SETNX key value

SETNX命令的作用是:只有当指定的键名 key 不存在时,将键值对存储到Redis数据库中。如果键名 key 已经存在,则不执行任何操作。

那么整体的核心思路就是:让当前线程尝试先创建A再执行业务逻辑代码,如果A不存在,就进行创建,并执行相关业务逻辑,业务逻辑执行完毕后释放A;如果A存在,那么说明此时有其他的线程在执行业务逻辑代码,则拒绝当前线程执行业务逻辑(挂起线程)

其实就是通过SETNX构造了一个唯一数据,并且把这个数据作为锁。这种思路使得我们的锁不再局限于某一个JAVA对象,从而避开了synchronized只能在JVM内部生效。解决了集群架构下多JVM上锁困难的困境

具体业务逻辑:

本次的具体业务应用场景是优惠卷秒杀场景,简单的来讲:就是商家发放优惠卷,用户进行抢购。而在优惠卷秒杀业务中,我们需要注意的是一人一单问题。一人一单就是一个用户只允许下一单。而我们本项目的背景是允许多端登录。我们可以想一想这个问题的核心问题:如果多端登录,在服务器集群架构的模式下,如果我们还是传统模式加锁,就会出现这个问题

用户A同时登录的电脑和手机,在以前的模式下:我们是简单粗暴的给一人一单核心代码直接解锁。但这样做有两个问题:

1.如果直接加锁,那么也就是说程序的并发性大大降低,我们一次只能处理一个用户的优惠卷订单,效率大大降低。

2.如果是在集群模式下,传统的锁只能在一个JVM内生效,并不能跨JVM。如果用户的电脑购买优惠卷请求进入到了服务器A,而用户的手机购买优惠卷请求进入到了服务器B,那么就有可能造成优惠卷超卖的情况。

总结一下优惠卷超卖场景的业务逻辑

  1. 查询优惠卷是否存在
  2. 查询优惠卷是否在售卖时间
  3. 查询当前优惠卷是否还有库存
  4. 查询用户是否已经下过单(如果有直接返回给前端Result,封装消息类)
  5. 扣减优惠卷库存
  6. 创建订单ID
  7. 返回订单号给前端
  8. 封装订单相关信息,更新数据库

在这几步中,从4-8步就是一人一单问题,而解决优惠卷秒杀问题,大部分情况就是在解决这个问题。

业务问题解决思路

我们来一步一步看当前有哪些问题需要我们解决:

1.选择加锁问题:

在我们最开始的加锁中,我们选择的是synchronized关键字,但是它会导致程序的并发性大大降低。并且无法跨JVM容器生效。

我们为了解决synchronized关键字无法跨JVM容器生效,采用了SETNX关键字。通过这种方法,我们解决了锁跨JVM容器生效。

synchronized 是基于JVM层面的同步机制,它会锁定整个方法,而且它的作用范围限定在单个JVM内。在分布式系统或者集群环境中,synchronized 不能跨JVM工作,因此不适合作为分布式锁使用。而分布式锁 simpleRedisLock 是基于Redis实现的,可以跨多个应用实例工作,适用于分布式系统。

但是它本质上和synchronized关键字的作用一样,并没有解决程序的并发性大大降低的问题。只不过以前我们是通过synchronized关键字拦截线程,现在是通过SETNX拦截线程。

那么让我们来逆推一下思路,加锁是为了解决两个问题:

  1. 同一用户在不同端多次购买的相同优惠卷的行为
  2. 不同用户同时购买同一优惠卷的行为。

而我们可以先来优化一下同一用户在不同端多购买的行为。按照我们之前的思路是不管三七二十一就上锁。如图所示可以理解为:

但是我们真的有这个必要嘛?我们仔细想一想:如果只是为了避免同一用户在不同端多次购买的相同优惠卷,那么我们只需要针对这个用户加锁不就好了嘛?

 也就是说:现在我们设计的锁,应该是只会拦截同一个用户的多次登录,而不拦截多个用户的并发登录。如图所示可以理解为:

我们从代码层面解释一下:我们利用SETNX创建key的时候,将key设置为USERID。那么此时就会出现两种情况:

1.同一用户多端登录发送购票请求,由于SETNX创建KEY的时候是根据UserID创建的,因此只能有一个端创建key成功,实现了为同一用户加锁,避免多端登录购票。

2.不同的用户由于UserID不同,因此SETNX创建KEY的时候不会失败,也就是说不会被拦截。

也就是说:我们通过根据UserID构造key的方式,实现了为每个用户加锁,提高了程序的并发性能。

我们再来解决一下:多个用户同时购买同一优惠卷的问题。我们再来转变一下角度:之所以要处理多个用户同时购买同一优惠卷,是因为会存在超卖问题。而我们如何除了加锁之外,还有没有其他的方法解决超卖问题呢?

答案是有的.我们在每一次扣减库存的时候,都同步判断一下当前数据库中优惠卷库存是否大于0不就好了嘛!

当然,这里要保证判断库存和扣减库存的原子性,不可以被打断。

其实这里的思路就是CAS算法,即Compare And Swap

那么选择加锁问题我们已经解决了,为了优化普通模式下加锁的无法跨JVM容器拷打并发性的问题,我们采用了以下两个步骤:

  • 无法跨容器:使用Redis中的SETNX来保证锁可跨JVM容器
  • 并发性差:利用userID构造每个用户专属的锁,并且通过数据库操作维护多用户下单超卖问题。

此时我们用流程图来展示一下当前的执行逻辑:

当然了,为了避免死锁的出现,我们要为SETNX构造出的键值对设置过期时间,防止死锁的出现。

而接下来的问题也就是我们要着重介绍的一个问题:

2.Redis分布式锁的误删问题:

此处我们说的是同一用户多端登录引发的并发性问题,而不同用户之间由于构造的时候key就不一样,因此不存在误删问题。

在我们前面构造的业务逻辑中,理想的状态应该是:

在理想状态下,多段登录可以正确的创建和释放锁,维护程序的并发性,而在我们的业务逻辑中,可能会出现如下异常情况:

这段异常简单的来讲:线程1的阻塞使得线程1所创建的用户锁被超时释放,此时Redis中并没有针对当前用户的锁,当前用户再发起一个线程2线程2获取到锁。而线程1此时阻塞结束,开始执行业务和最后删除锁的操作,导致线程2创建的当前用户锁被删除。此时线程2在执行自己的业务,但是整个redis中已经无针对当前用户的锁了。线程3此时尝试获取锁,获取成功。那么在这种环境下,线程1,2,3都获取到了锁并且执行了买票业务。

这种业务场景虽然少见,但仍是我们要解决的问题。

而解决的思路也很简单:主要的思路:设置锁标识,让每个线程只能删除自己的锁 

也就是说:以前我们利用SETNX创建锁的时候,是不管锁的value值的,现在为了解决锁的误删问题,我们要给value中赋值,使其成为锁标识。

我们看看代码:

创建锁:

删除锁:

但是这样就对了嘛? 

其实是不对的! 这是因为我们在unlock里面执行了多条语句,可能在获取锁的标识的时候,还没来得及执行delete语句,线程就又被阻塞了,此时就又会发生我们之前说的误删问题。

3,如何保证删除锁代码的原子性?

在这里我们使用的是lua脚本。Redis提供了lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

关于lua脚本的书写我们这里不做具体介绍,感兴趣的同学可以自学,lua是基于c语言实现的,他的语法结构很简单。

Lua 教程 (w3schools.cn)icon-default.png?t=N7T8https://www.w3schools.cn/lua/index.asp

将之前的unlock中的redis操作转化为lua脚本,然后再交给redis执行。

我们来看看代码:

通过这种方式,我们就确保了多条Redis命令的原子性,解决了删除锁代码的原子性问题。

业务杂项知识点:

1.Spring mvc中的事务失效引起的并发问题:

在代码框架设计的时候,我把4-8过程单独拉出来封装了一个方法:

封装部分代码: 

为了保证扣减库存的时候执行的多条SQL语句的原子性,我们加上了@Transactional注解。然后在获取锁后执行业务逻辑代码的时候调用这个方法。

但这也就是一个坑点:Spring mvc中的事务是会失效的。 

        在Spring框架中,声明式事务管理依赖于AOP(面向切面编程)。当我们在一个方法上使用@Transactional注解时,Spring将创建一个代理对象来包装原始的Bean。这个代理对象会在方法调用前后添加事务管理的逻辑,如开启和关闭事务,以及在发生异常时进行回滚操作。

如果直接调用同一个类中的另一个@Transactional方法,由于是内部调用,并不会经过代理对象,因此事务管理相关的逻辑不会被执行。这就是为什么通常建议将事务管理放在服务层(Service Layer),并且只通过注入的方式跨类调用事务方法,确保每次调用都能通过代理对象,从而让AOP能够正确地应用事务管理的逻辑。

如果不使用Spring AOP代理机制,那么@Transactional注解将不会生效,因为没有任何机制来拦截方法调用并应用事务的边界。这意味着即使定义了事务,也不会有实际的事务行为发生,如开始新事务、加入现有事务或在发生异常时回滚事务。

总结来说,Spring的声明式事务管理是通过AOP代理实现的,不使用AOP代理将导致事务失效。要确保事务能够正常工作,必须遵循Spring的配置和使用准则,确保通过代理对象对事务方法进行调用。

因此在调用这个方法时候,我们不能直接调用,这种方式是错误的! 

而应该这么调用:

 

2.包装类与基本数据类型的差异:

当我们使用stringRedisTemplate来操作Redis的时候,返回值会有包装类型,例如Boolean。

但是如果我们直接这样返回的话,会出现一个问题:我们要求的返回值类型是boolean,也就是基本数据类型。虽然Boolean会有自动拆箱功能,可以自动转换为boolean,但是可能会出现空指针异常!

这是为什么呢?原因很简单:Boolean是包装类,可以存放空值,而在自动拆箱的时候空值会转变为空指针。而基本数据类型不允许存储空指针。因此直接抛出空指针异常。

总结:

        经过本文的讲解,我们了解了如何利用Redis实现一个简单的分布式锁。而其实Redis就已经为我们提供了一套高性能,高可用的分布式锁:Redission。在之后的文章我也会给大家介绍如何使用Redission。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

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

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

相关文章

Spring Security 自定义异常失效?源码分析与解决方案

🚀 作者主页: 有来技术 🔥 开源项目: youlai-mall 🍃 vue3-element-admin 🍃 youlai-boot 🌺 仓库主页: Gitee 💫 Github 💫 GitCode 💖 欢迎点赞…

python pyaudio对音频进行端点检测,检测出说话区间

python pyaudio对音频进行端点检测,检测出说话区间 主要采用过零率和语音能量来进行检测,并设置双阈值。 代码如下: # -*- coding: utf-8 -*- import wave import os import matplotlib.pyplot as plt import numpy as np# 判断是否变号 de…

差分基准站

差分基准站,又称参考接收机,是一种固定式卫星接收机,用于提高卫星定位精度。 差分基准站的作用是提供已知位置和准确的位置信号,以纠正其他移动定位终端接收器接收到的卫星信号中的误差。 卫星定位信号会受到多种因素的影响&#…

Redis缓存——Spring Cache入门学习

Spring Cache 介绍 Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。 Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如: EHCacheCaffeineR…

JAVA-作业7-画一个笑脸

要求如题 代码如下: SmileFace01: import java.awt.Color; import java.awt.Graphics;import javax.swing.JPanel;public class SmileFace01 extends JPanel {Overrideprotected void paintComponent(Graphics g) {super.paintComponent(g);int width getWidth(…

【c】有序数列插入一个整数

#include<stdio.h> int main() {int n;scanf("%d",&n);int arr[n1];for(int i0;i<n;i){scanf("%d",&arr[i]);}int a;scanf("%d",&a);arr[n]a;for(int j0;j<n;j){if(arr[j]>arr[n])//交换元素位置{int temparr[j];arr…

【广州华锐互动】风电场检修VR情景模拟提供接近真实的实操体验

风电场检修VR情景模拟系统由广州华锐互动开发&#xff0c;这是一种新兴的培训方式&#xff0c;它通过虚拟现实技术将风力发电场全范围进行1:1仿真建模还原&#xff0c;模拟监视风力发电场各种运行工况下的运行参数和指标&#xff0c;同时可进行升压站系统的巡视&#xff0c;倒闸…

JavaWeb 分页查询

由于html不能直接从域当中直接拿数据 所以我们引入了jsp文件 数据存在了requets域当中 如果数据量很大,不可能把所有数据全部在页面展示: 数据全部在页面展示缺点: SQL执行时间过长 用户查看数据,滚动滚动条,用户体验不高 在实际开发中,分页查询&#xff0c; 实现: sql语句…

什么是Overlay网络?Overlay网络与Underlay网络有什么区别?

你们好&#xff0c;我的网工朋友。 在传统历史阶段&#xff0c;数据中心的网络是以三层架构&#xff08;核心、汇聚、接入&#xff09;为基本标准。 但是随着技术的发展&#xff0c;不同的厂家有不同的组建方式&#xff0c;比如说在核心层、汇聚层和接入层增加虚拟化技术。 …

Linux Namespace技术

对应到容器技术&#xff0c;为了隔离不同类型的资源&#xff0c;Linux 内核里面实现了以下几种不同类型的 namespace。 UTS&#xff0c;对应的宏为 CLONE_NEWUTS&#xff0c;表示不同的 namespace 可以配置不同的 hostname。User&#xff0c;对应的宏为 CLONE_NEWUSER&#xf…

Vmware17虚拟机安装windows10系统

不要去什么系统之家之类的下载镜像&#xff0c;会不好安装&#xff0c;镜像被魔改过了&#xff0c;适合真实物理机上的系统在PE里安装系统&#xff0c;建议下载原版系统ISO文件 安装vmware17pro 下载地址https://dwangshuo.jb51.net/202211/tools/VMwareplayer17_855676.rar 解…

【源码解析】聊聊线程池 实现原理与源码深度解析(二)

AbstractExecutorService 上一篇文章中&#xff0c;主要介绍了AbstractExecutorService的线程执行的核心流程&#xff0c;execute() 这个方法显然是没有返回执行任务的结果&#xff0c;如果我们需要获取任务执行的结果&#xff0c;怎么办&#xff1f; Callable 就是一个可以获…

【yolov8】与yolov5的区别及改进详解

图像识别技术在物联网、智能监控等领域广泛应用。而深度学习中的目标检测技术&#xff0c;能够帮助我们对图像中的目标进行识别&#xff0c;进而实现自动化控制。目前&#xff0c;Yolov8和Yolov5是目标检测领域热门的模型。 yolo目标检测原理yolov5详解yolov8yolov8结构图Conv模…

RK3588+MCU机器人控制器解决方案

1 产品简介 XMP04A 是一款信迈科技基于 RK3588 设计的高性能、低功耗的边缘计算设备&#xff0c; 内置 NPU 算力可达 6.0TOPSINT8&#xff0c;以及具备强大的视频编解码能力&#xff0c;最高可支持 32 路 1080P30fps 解码和 16 路 1080P30fps 编码 &#xff0c;支持 4K12…

数据库管理-第120期 初探Halo数据库(202301201)

数据库管理-第120期 初探Halo数据库&#xff08;202301201&#xff09; 12月份正好也是第120期&#xff0c;新的一篇文章&#xff0c;尝试一条新的路线。其实吧&#xff0c;Halo&#xff08;羲和&#xff09;这个数据库我较早时间就听说过&#xff08;早于今年DTCC&#xff0c…

SpringBoot+SSM项目实战 苍穹外卖(3)

继续上一节的内容&#xff0c;本节完成菜品管理功能&#xff0c;包括公共字段自动填充、新增菜品、菜品分页查询、删除菜品、修改菜品。 目录 公共字段自动填充新增菜品文件上传实现新增菜品实现 useGeneratedKeys 菜品分页查询删除菜品修改菜品根据id查询菜品实现修改菜品实现…

【Go】Go语言基础内容

变量声明&#xff1a; 变量声明&#xff1a;在Go中&#xff0c;变量必须先声明然后再使用。声明变量使用 var 关键字&#xff0c;后面跟着变量名和类型&#xff0c;如下所示&#xff1a; var age int这行代码声明了一个名为 age 的整数变量。 变量初始化&#xff1a;您可以在声…

JFrog----SBOM清单包含哪些:软件透明度的关键

文章目录 SBOM清单包含哪些&#xff1a;软件透明度的关键引言SBOM清单的重要性SBOM清单包含的核心内容SBOM的创建和管理结论 软件物料清单&#xff08;SBOM&#xff09;是一个在软件供应链安全中越来越重要的组成部分。它基本上是一份清单&#xff0c;详细列出了在特定软件产品…

ENVI植被指数阈值法

植被指数阈值法提取纯净像元 首先用ENVI打开无人机遥感影像 1. 假彩色显示 打开数据管理工具&#xff0c;无人机的4波段为红边波段 2. 波段计算 打开band math&#xff0c;输入 float(b1-b2)/(b1b2) 选择对应波段 3. 阈值筛选 阈值按经验值选的0.7&#xff0c;ndvi…

从零开始实现神经网络(二)_CNN卷积神经网络

参考文章: 介绍卷积神经网络1 介绍卷积神经网络2 在过去的几年里&#xff0c;关于卷积神经网络&#xff08;CNN&#xff09;的讨论很多&#xff0c;特别是因为它们彻底改变了计算机视觉领域。在这篇文章中&#xff0c;我们将建立在神经网络的基本背景知识的基础上&#xff0c;探…