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

news2025/1/10 16:43:12

一、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>() {
                @Override
                public 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>() {
                @Override
                public 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.coloradmin.cn/o/1282346.html

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

相关文章

机器人制作开源方案 | 校园餐具回收分类机器人

作者&#xff1a;梁桥、吴振宇、凌福海、李清轩、姜晓敏 单位&#xff1a;华北科技学院 指导老师&#xff1a;韩红利、张伟杰 1. 场景调研 1.1 项目实施目的 受新冠病毒引起的影响&#xff0c;人们生产生活发生了巨大的改变。现处于疫情防控常态化阶段&#xff0c;为应对点状…

基于Python实现的滑动验证码自动识别工具源码

滑动验证码识别 今天的目标地址是字节的巨量纵横&#xff0c;目前东家是一家广告营销型的公司&#xff0c;专注于在各大平台投放信息流广告。巨量纵横为字节跳动的广告平台&#xff0c;用于管理推广账户。今天破解一下这个平台的登陆入口&#xff0c;为今后的数据爬取开个头。…

IEEE 机器人最优控制开源库 Model-based Optimization for Robotics

系列文章目录 文章目录 系列文章目录前言一、开源的库和工具箱1.1 ACADO1.2 CasADi1.3 Control Toolbox1.4 Crocoddyl1.5 Ipopt1.6 Manopt1.7 LexLS1.8 NLOpt1.9 qpOASES1.10 qpSWIFT1.11 Roboptim 二、其他库和工具箱2.1 MUSCOD2.2 OCPID-DAE12.3 SNOPT 前言 机器人&#xff…

网工学习9-STP配置(二)

如图 1 所示&#xff0c;当前网络中存在环路&#xff0c; SwitchA 、SwitchB 、SwitchC 和 SwitchD 都运行 STP&#xff0c;通过 彼此交互信息发现网络中的环路&#xff0c;并有选择的对某个端口进行阻塞&#xff0c;最终将环形网络结构修剪成无 环路的树形网络结构&#xff…

山西电力市场日前价格预测【2023-12-04】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-12-04&#xff09;山西电力市场全天平均日前电价为179.48元/MWh。其中&#xff0c;最高日前电价为362.01元/MWh&#xff0c;预计出现在18:00。最低日前电价为0.00元/MWh&#xff0c;预计出…

【c】课程满意度计算

我们不好直接比较二维数组中任意多个元素的值是否相等&#xff0c;我们可以创建一维数组&#xff0c;首先将一维数组的值全部设为0&#xff0c;一维数组的下标代表你喜欢课程的量&#xff0c;一维数组的各个元素的值代表你喜欢的次数 例如 你输入3 5&#xff0c;代表你喜欢第三…

“数”说新语向未来 | GBASE南大通用2023媒体交流会成功举办

在当前国家信创战略加速实施&#xff0c;及国民经济数字化转型&#xff0c;叠加驱动信息化行业加速发展的大形势下&#xff0c;以“数说新语-GBASE南大通用开放创新再领航”为主题的2023 GBASE南大通用媒体交流日活动在GBASE天津总部举行。来自IT168、ITPUB、韩锋频道、自主可控…

【每日OJ —— 110. 平衡二叉树】

每日OJ —— 110. 平衡二叉树 1.题目&#xff1a;110. 平衡二叉树2.解法2.1.算法讲解2.2.代码实现2.3.提交通过展示 1.题目&#xff1a;110. 平衡二叉树 2.解法 2.1.算法讲解 1.这道题中的平衡二叉树的定义是&#xff1a;二叉树的每个节点的左右子树的高度差的绝对值不超过 11…

国内的几款强大的AI智能—AI语言模型

R5Ai智能助手是一款由百度研发的文心一言&#xff0c;它支持gpt4 / gpt-3.5 / claude&#xff0c;也支持AI绘画&#xff0c;每天提供十次免费使用机会&#xff0c;无需魔法。该智能助手具有以下优点&#xff1a;会画画&#xff0c;没有使用次数限制&#xff0c;可以在界面上找到…

二蛋赠书十期:《剪映短视频剪辑从入门到精通》

前言 大家好&#xff01;我是二蛋&#xff0c;一个热爱技术、乐于分享的工程师。在过去的几年里&#xff0c;我一直通过各种渠道与大家分享技术知识和经验。我深知&#xff0c;每一位技术人员都对自己的技能提升和职业发展有着热切的期待。因此&#xff0c;我非常感激大家一直…

C/C++,图算法——凸包的快速壳(Quick Hull)算法的源代码

1 文本格式 // C program to implement Quick Hull algorithm // to find convex hull. #include<bits/stdc.h> using namespace std; // iPair is integer pairs #define iPair pair<int, int> // Stores the result (points of convex hull) set<iPair>…

(c语言进阶)结构体内存对齐和修改默认对齐数

一.结构体内存对齐 结构体内存大小计算方法&#xff1a; 偏移量&#xff1a;是指某个成员在结构体中相对于结构体首地址的偏移字节数。在计算机中&#xff0c;结构体是一种自定义数据类型&#xff0c;它由多个不同类型的成员组成。每个成员在内存中的存储位置是连续的&#xf…

大数据|计算机毕业设计——基于Django协同过滤算法的房源可视化分析推荐系统的设计与实现

大数据|计算机毕业设计——基于Django协同过滤算法的房源可视化分析推荐系统的设计与实现 技术栈&#xff1a;大数据爬虫/机器学习学习算法/数据分析与挖掘/大数据可视化/Django框架/Mysql数据库 本项目基于 Django框架开发的房屋可视化分析推荐系统。这个系统结合了大数据爬…

企业培训私有化解决方案PlayEdu

本文应网友 林枫 的要求而折腾&#xff1b; 什么是 PlayEdu &#xff1f; PlayEdu 是一款适用于搭建内部培训平台的开源系统&#xff0c;旨在为企业/机构打造自己品牌的内部培训平台。PlayEdu 基于 Java MySQL 开发&#xff1b;采用前后端分离模式&#xff1b;前端采用 React1…

在 App 设计工具的代码视图中管理代码

目录 管理组件、函数和属性 识别代码中的可编辑部分 编写 App 管理 UI 组件 管理回调 在 App 中共享数据 在多个位置运行的单一源代码 创建输入参数 为您的 App 添加帮助文本 限制您的 App 一次只运行一个实例 修复代码问题和运行时错误 个性化代码视图外观 更改颜…

SourceTree for Mac: 您的个人Git仓库管理专家

在当今的软件开发世界中&#xff0c;版本控制系统如Git的重要性日益凸显。它们帮助开发者在协作开发过程中保持代码的同步和有序。如果你是一位Mac用户&#xff0c;并且正在寻找一款简单易用的Git客户端工具&#xff0c;那么SourceTree for Mac可能是你的最佳选择。 SourceTre…

服务异步通讯

四、服务异步通讯 4.1初始MQ 4.1.1同步通讯和异步通讯 同步调用的优点: 时效性较强,可以立即得到结果 同步调用的问题: 耦合度高 性能和吞吐能力下降 有额外的资源消耗 有级联失败问题 异步通信的优点: 耦合度低 吞吐量提升 故障隔离 流量削峰 异步通信的缺点: …

Python更改YOLOv5、v7、v8,实现调用val.py或者test.py后生成pr.csv,然后再整合绘制到一张图上(使用matplotlib绘制)

1. 前提 效果图 不错的链接&#xff1a;YOLOV7训练模型分析 关于map的绘图、loss绘图&#xff0c;可参考&#xff1a;根据YOLOv5、v8、v7训练后生成的result文件用matplotlib进行绘图 v5、v8调用val.py&#xff0c;v7调用test.py&#xff08;作用都是一样的&#xff0c;都是…

根据YOLOv5、v8、v7训练后生成的result文件用matplotlib进行绘图

1. 效果图 2. 认识result内容 2.1 YOLOv7的result.txt 参考链接&#xff1a;YOLOv7结果分析&#xff0c;txt文件内容 0/299 14.7G 0.07522 0.009375 0.02266 0.1073 58 640 0.0002958 0…

【开源】基于JAVA的考研专业课程管理系统

项目编号&#xff1a; S 035 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S035&#xff0c;文末获取源码。} 项目编号&#xff1a;S035&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 考研高校模块2.3 高…