SpringBoot框架结合Redis实现分布式锁

一、SpringBoot结合 Redis实现分布式锁

1.1、什么是分布式锁

分布式锁,是在分布式的环境下,才会使用到的一种同步访问机制,在传统的单体环境里面,不存在分布式锁的概念,只有在分布式环境里面,才有分布式锁的概念,那到底什么是分布式锁呢?

现在我们通过一个案例来看下,分布式锁的作用。

  • 假设,有一个应用程序,它是分布式部署的,总共有两个应用实例部署在两台机器上面。
  • 注意:这两个应用程序是一模一样的,只不过部署的机器不同。
  • 现在,假设应用程序中有一个定时任务,是专门用于操作数据库的数据,那么,当定时任务执行的时候,两台机器上面的程序都会同时执行,也就是说,此时,存在两个任务同时操作同一个数据库的数据,如果不加锁,那么数据库的数据就会被操作两次。
  • 这显然,可能会出现问题啦,例如:如果是新增数据,那就会插入两条数据,但是实际情况下,我们希望是只有一条数据。
  • 要实现这个功能,就需要添加分布式锁,只有获取到锁的那台机器,才能够操作数据库,其他的机器就不能操作。

分布式锁如下图所示:

在这里插入图片描述

1.2、如何实现分布式锁

使用Redis数据库实现分布式锁,核心思想就是:

  • 第一步:每一个线程访问共享资源之前,都需要向redis里面设置一个分布式锁的key。
  • 第二步:如果分布式锁的key已经存在,则说明其他线程已经拿到锁了,那么当前线程就获取锁失败,不用执行。
  • 第三步:如果当前线程获取锁成功,则执行具体的业务逻辑代码。
  • 第四步:当业务逻辑代码执行完成之后,此时,需要主动将分布式锁key给删除掉,这样,下次访问的时候,其他线程可以有机会获取到锁。
  • 注意:删除锁的时候,一定只能够删除当前线程设置的锁,其他线程不能够删除非自身线程的锁。

1.3、分布式锁实现代码

这里是采用SpringBoot结合Redis实现分布式锁,所以,我们需要搭建SpringBoot环境,以及集成Redis数据库。

(1)创建RedisLock工具类

这里我们创建一个RedisLock类,这个类是专门用于加锁、解锁操作的。

  • 为了保证加锁、解锁操作的原子性,一般实际开发中,都会采用LUA脚本执行redis命令。
  • 注意:这里删除锁的时候,必须只能删除当前线程持有的锁。
  • 为什么呢???
  • 假设:A线程获取到了锁,开始执行业务代码,由于执行业务代码时间很长,此时,锁已经过期了,redis已经删除了过期的key;
  • 如果这个时候,B线程来获取锁,那是可以获取成功的,即:B获取锁成功,此时redis中的锁是B持有的。
  • 但是当A线程的业务代码执行完成之后,最后要删除锁,假设,删除锁之前,没有判断这个锁是不是A线程的,就直接删除了,那么此时A线程删除的锁是B线程持有的,从而导致错误。
  • 所以,每次删除锁的时候,都需要判断一下,当前的锁是不是自己持有的。
package com.spring.boot.demo.utils;import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.commands.JedisCommands;
import redis.clients.jedis.params.SetParams;import java.util.ArrayList;
import java.util.List;
import java.util.Objects;/*** @author ZhuYouBin* @version 1.0.0* @Date: 2022/11/8 20:41* @Description Redis分布式锁工具类*/
@Component
public class RedisLock {// 定义 RedisTemplate 对象private static RedisTemplate<String, Object> redisTemplate;// 通过构造方法的方式,注入 RedisTemplate 对象public RedisLock(RedisTemplate<String, Object> redisTemplate) {RedisLock.redisTemplate = redisTemplate;}// 创建 LUA 脚本【用于解锁的脚本语句】private static final StringBuilder unlock_lua = new StringBuilder();static {// TODO 这里的 KEYS[1] 表示分布式锁对应的 key 值// TODO 这里的 ARGV[1] 表示当前线程的唯一标识,要和分布式锁对应的 value 值比较是否相等// 查询当前 key 对应的 value 值,是否为当前线程拥有的unlock_lua.append("if redis.call(\"get\", KEYS[1]) == ARGV[1] ");unlock_lua.append("then "); // 当前锁是当前线程拥有的,可以删除分布式锁unlock_lua.append("   return redis.call(\"del\", KEYS[1]) ");unlock_lua.append("else "); // 不是当前线程拥有的锁,删除失败unlock_lua.append("   return 0 ");unlock_lua.append("end ");}/*** 获取分布式锁的操作* @param key 锁对应的 key 值* @param requestId 当前请求线程唯一标识, 作为 value 值【例如:UUID】* @param expire 锁过期时间,单位ms* @return 加锁成功,则返回 true*/public static boolean acquireLock(final String key, final String requestId, final long expire) {try {RedisCallback<String> callback = new RedisCallback<String>() {@Overridepublic String doInRedis(RedisConnection redisConnection) throws DataAccessException {// 获取 redis 命令对象JedisCommands commands = (JedisCommands) redisConnection.getNativeConnection();SetParams params = new SetParams();params.nx(); // 只有键不存在,才能够设置params.px(expire); // 设置过期时间,单位ms// 执行命令【】return commands.set(key, requestId, params);}};// 执行加锁命令Object result = redisTemplate.execute(callback);if (!Objects.isNull(result)) {// 执行结果不等于null,则说明执行成功return true;}} catch (Exception e) {e.printStackTrace();}return false;}/*** 释放当前线程持有的分布式锁* @param key 锁对应的 key 值* @param requestId 当前请求线程唯一标识, 作为 value 值* @return 释放成功,则返回true*/public static boolean releaseLock(final String key, final String requestId) {try {final List<String> keys = new ArrayList<>();keys.add(key);final List<String> args = new ArrayList<>();args.add(requestId);RedisCallback<Long> callback = new RedisCallback<Long>() {@Overridepublic Long doInRedis(RedisConnection redisConnection) throws DataAccessException {Object nativeConnection = redisConnection.getNativeConnection();Jedis jedis = (Jedis) nativeConnection;// 执行解锁脚本语句Object eval = jedis.eval(unlock_lua.toString(), keys, args);return (Long) eval;}};Long ret = (Long) redisTemplate.execute(callback);if (ret != null && ret > 0L) {// 删除成功,解锁成功return true;}} catch (Exception e) {e.printStackTrace();}return false;}}

1.4、编写测试案例

为了演示分布式锁的功能,这里可以编写两个应用程序,写两个定时任务,分别用于模拟同时访问数据库的情况,案例代码如下所示:

(1)案例代码

package com.spring.boot.demo.controller;import com.spring.boot.demo.utils.RedisLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;/*** @author ZhuYouBin* @version 1.0.0* @Date: 2022/11/9 22:18* @Description*/
@Component
public class TaskDemo01 {@Scheduled(cron = "0/10 * * * * ?")public void demo01() throws InterruptedException {final String key = "distribute_lock_key";final String requestId = UUID.randomUUID().toString();final long expire = 10000; // 10s过期boolean acquireLock = RedisLock.acquireLock(key, requestId, expire);SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String format = sdf.format(new Date());if (acquireLock) {System.out.println(format + ": [TaskDemo01]模拟操作数据库......");// TODO 暂停2秒,释放锁Thread.sleep(2000);RedisLock.releaseLock(key, requestId);} else {System.out.println(format + ": [TaskDemo01]获取锁失败,不执行......");}}}

(2)运行测试

将上面的工程分别启动两个实例,然后查看控制台的输出日志。

在这里插入图片描述

到此,Redis实现分布式锁就成功啦。

1.5、Redis分布式锁存在的问题

虽然这种Redis实现分布式锁可以解决大部分的问题,但是仍然可能存在问题:

  • 分布式锁过期问题。
    • 当某个线程执行业务逻辑代码的时间,超过分布式锁的有效时间,此时其他线程会获取到锁。
    • 解决办法:使用redission提供的看门狗进行key续期操作。
  • 多个Redis实例情况。
    • 当项目中存在多个Redis实例时候,此时这种分布式锁就失效了。
    • 解决办法:使用redission实现分布式锁。

这种分布式锁可以说,在大部分情况下足够使用了,但是,当项目中部署了多个redis实例时候,这种分布式锁就失效了,多个redis实例时候,可以使用redis官方推出的RedLock。

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

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

相关文章

赴日程序员高年薪过上“躺平”生活?

日本的IT行业想要达到的高薪&#xff0c;也是需要很多资历和经验的&#xff0c;不过即使你是新卒&#xff0c;也能拿到相比国内来说让你满意的薪资。 刚入职的起薪是20-23万日元/月&#xff0c;情报信息业出身&#xff0c;技术掌握不错&#xff0c;起薪是25万-30万日元。之后经…

git的安装及ssh配置(Linux)

环境 CentOS Linux release 7.9.2009 (Core) Xftp7 安装 方法一&#xff1a;yum安装 yum是一个客户端软件&#xff0c;就好比手机上的应用商店&#xff0c;帮助我们对软件的下载、安装和卸载 1、首先查看自己是否安装过git [rootxiaoxi ~]# git -bash: git: command not fo…

C++继承(详解)

一、继承的概念 1.1、继承的概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称派生类。继承呈现了面向对象程序设计的层次结…

【JavaEE】单例模式

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文于《JavaEE》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&…

Python容器——字典

Key——Value 键值对

科技云报道:AI+PaaS,中国云计算市场迎来新“变量”?

科技云报道原创。 没有小的市场&#xff0c;只有还没有被发现的大生意。 随着企业数字化转型的逐级深入&#xff0c;市场需求进一步向PaaS和SaaS层进发&#xff0c;使之成为公有云服务市场增长的主要动力。 根据IDC最新发布的报告显示&#xff0c;2022-2027五年间中国公有云…

初识计算机网络

网络通信基础 1. IP地址2.端口号3.认识协议3.1协议分层 4. 网络数据传输的基本流程4.1 五元组4.2封装和分用 1. IP地址 IP地址主要用于表示网络主机,其他网络设备的网络地址,IP地址用于定位主机的网络地址 比如:发送快递的时候,需要知道对象的收货地址,才能将包裹送到目的地. …

APISpace 实名认证(身份证二要素)接口案例代码

1.实名认证&#xff08;身份证二要素&#xff09;API APISpace 的 实名认证&#xff08;身份证二要素API&#xff09;&#xff0c;核验身份证二要素&#xff08;姓名和身份证号码&#xff09;信息是否一致。 2.实名认证&#xff08;身份证二要素&#xff09;接口详情 2.1 接口…

外汇天眼:CySEC宣布与Titanedge Securities 达成90,000欧元的和解

塞浦路斯证券交易委员会&#xff08;CySEC&#xff09;12月1日宣布已经与塞浦路斯投资公司Titanedge Securities Ltd 达成了一项和解。 此次和解涉及可能违反了2017年《投资服务和活动以及受监管市场法》的情况。更具体地说&#xff0c;达成和解的调查涉及评估该公司在2017/565…

自动化测试的4大注意事项

自动化测试能够提高测试效率、覆盖率&#xff0c;降低测试成本和工作量&#xff0c;是软件开发中不可或缺的一部分。但前提是要确保自动化测试的有效性和可靠性&#xff0c;否则无效或错误的自动化测试&#xff0c;往往会对项目造成负面影响&#xff0c;如维护成本高、假阳性和…

高等职业学校新媒体营销实训室解决方案

背景 随着数字化时代的来临&#xff0c;新媒体营销成为企业推广和品牌建设的关键手段。为了培养高职学生在新媒体领域的实际操作能力&#xff0c;建立一套全面、系统的实训室方案至关重要。 目标 搭建高职新媒体营销实训室&#xff0c;旨在培养学生的实际操作能力&#xff0…

这些B端产品设计规范,你都知道吗?

设计规范虽然有其通用性&#xff0c;但因应对不同的业务环境和企业形态&#xff0c;其具体的运用可能会有所差异。对于新入行的B端设计师&#xff0c;各种B端组件可能会让他们感到困惑&#xff0c;不知在何种场景下应选择何种组件。这主要是因为我们在日常中学到的B端知识点多是…

人工智能与供应链行业融合:开启智能化供应链的新时代

随着人工智能技术的快速发展&#xff0c;供应链行业正迎来革命性变革。本文将探索人工智能在供应链管理中的应用领域&#xff0c;并分析其带来的益处和挑战&#xff0c;展望人工智能与供应链融合的未来发展趋势。 引言 供应链管理是企业运营中不可或缺的重要组成部分。它涵盖了…

用友NC word.docx接口存在任意文件读取漏洞

声明 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。 一、产品介绍 用友 NC Cloud&#xff0c;大型企业数字化平台&#xff…

如何在Linux上搭建本地Docker Registry镜像仓库并实现公网访问

Linux 本地 Docker Registry本地镜像仓库远程连接 文章目录 Linux 本地 Docker Registry本地镜像仓库远程连接1. 部署Docker Registry2. 本地测试推送镜像3. Linux 安装cpolar4. 配置Docker Registry公网访问地址5. 公网远程推送Docker Registry6. 固定Docker Registry公网地址…

地方公派|商学院老师对口加拿大古德曼商学院访学交流

L老师荣幸地入选某省中青年教师国外访学进修计划&#xff0c;但因DIY申请职位无果&#xff0c;求助于我们。最终我们克服干扰因素&#xff0c;为其对口落实了加拿大最具声望和影响力的商学院之一布鲁克大学-古德曼商学院&#xff08;Goodman School of Business&#xff09;。 …

本地存储与复杂数据类型转换

1. 本地存储介绍 2.1 本地存储分类 - localStorage // 存储一个名字localStorage.setItem(uname, abc)// 获取名字console.log(localStorage.getItem(uname));// 删除本地存储 只删名字// localStorage.removeItem(uname)// 改localStorage.setItem(uname, aaa)// 存一个年龄 …

怎么翻译英文医学文献资料

文献翻译是一项要求严谨、精确且地道的工作&#xff0c;对于医学文献翻译更是如此。那么&#xff0c;怎么翻译英文医学文献资料&#xff0c;医学英文文献翻译公司哪个好&#xff1f; 专业人士指出&#xff0c;在翻译医学文献时&#xff0c;理解原文的语境是至关重要的。这不仅需…

【改进YOLOV8】融合动态蛇形卷积&DCNV2的草莓分级分割分割系统

1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 研究背景与意义 随着计算机视觉技术的不断发展&#xff0c;图像分割成为了一个重要的研究领域。图像分割可以将图像中的不同对象或区域进行分离&#xff0c;从而更好地理解图像内…

【数据结构(六)】排序算法介绍和算法的复杂度计算(1)

文章目录 1. 排序算法的介绍1.1. 排序的分类 2. 算法的时间复杂度2.1. 度量一个程序(算法)执行时间的两种方法2.2. 时间频度2.2.1. 忽略常数项2.2.2. 忽略低次项2.2.2. 忽略系数 2.3. 时间复杂度2.4. 常见的时间复杂度2.5. 平均时间复杂度和最坏时间复杂度 3. 算法的空间复杂度…