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>() {
                @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。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Python】tensorflow学习的个人纪录(3)

sess tf.Session()actor Actor(sess, n_featuresN_S, lrLR_A, action_bound[-A_BOUND, A_BOUND])步进&#xff1a;

工具网站:随机生成图片的网站

一个随机生成图片的网站&#xff1a;Lorem Picsum。 有时候&#xff0c;我们做静态页面需要大量图片去填充内容&#xff0c;以使用该网站去生成指定尺寸的图片。每次打开页面都会获取不同的图片&#xff0c;就不用我们做静态页面开发的时候&#xff0c;绞尽脑汁去找图片了。 …

【滑动窗口】水果成篮

水果成篮 904. 水果成篮 - 力扣&#xff08;LeetCode&#xff09; 文章目录 水果成篮题目描述问题转化 算法原理解法一解法二 代码编写C代码&#xff1a;使用容器数组模拟哈希表 Java代码使用容器数组模拟哈希表 题目描述 你正在探访一家农场&#xff0c;农场从左到右种植了一…

【hacker送书活动第7期】Python网络爬虫入门到实战

第7期图书推荐 内容简介作者简介大咖推荐图书目录概述参与方式 内容简介 本书介绍了Python3网络爬虫的常见技术。首先介绍了网页的基础知识&#xff0c;然后介绍了urllib、Requests请求库以及XPath、Beautiful Soup等解析库&#xff0c;接着介绍了selenium对动态网站的爬取和S…

红队攻防实战之Access注入

若盛世将倾&#xff0c;深渊在侧&#xff0c;我辈当万死以赴 访问漏洞url: 1.Access联合查询 判断是否有注入 and 11正常&#xff0c;and 12出错 判断字段数 order by 7正常 order by 8出错 爆破出表名并判断回显点为2&#xff0c;5 查看字段内容&#xff0c;将字段名填入回…

37. 解数独

题目描述 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff09; …

思维模型 反馈效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。反馈促进改进。 1 反馈效应的应用 1.1 反馈效应在营销中的应用 1 “可口可乐与百事可乐之战” 在 20 世纪 80 年代&#xff0c;可口可乐公司是全球最大的饮料公司之一&#xff0c;其市场…

常见的线程安全问题及解决

1. 什么是线程安全 线程安全指的是当多个线程同时访问一个共享的资源时&#xff0c;不会出现不确定的结果。这意味着无论并发线程的调度顺序如何&#xff0c;程序都能够按照设计的预期来运行&#xff0c;而不会产生竞态条件&#xff08;race condition&#xff09;或其他并发问…

算法设计与实现--贪心篇

贪心算法 贪心算法是一种在每一步选择中都采取当前状态下最优决策的算法&#xff0c;以期望能够通过一系列局部最优的选择达到全局最优。贪心算法的关键是定义好局部最优的选择&#xff0c;并且不回退&#xff0c;即一旦做出了选择&#xff0c;就不能撤销。 一般来说&#xf…

洛谷 P5711 闰年判断 C++代码

目录 前言 思路点拨 AC代码 结尾 前言 今天我们来做洛谷上的一道题目。 网址&#xff1a;【深基3.例3】闰年判断 - 洛谷 题目&#xff1a; 思路点拨 首先题目让我们输入一个年份&#xff0c;因此我们需要定义一个变量year&#xff0c;来存储输入的年份&#xff1a; in…

Bean的加载控制

Bean的加载控制 文章目录 Bean的加载控制编程式注解式ConditionalOn*** 编程式 public class MyImportSelector implements ImportSelector {Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {try {Class<?> clazz Class.forName("…

Nginx 具体应用

1 Nginx 1.1 介绍 一款轻量级的 Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。它占有的内存少&#xff0c;并发能力强&#xff0c;中国大陆使用 nginx 的网站有&#xff1a;百度、京东、新浪、网易、腾讯、淘宝等。第一个公开版本发布于…

Flyway 数据库版本管理 | 专业解决方案

前言 目前很多公司都是通过人工去维护、同步数据库脚本&#xff0c;但经常会遇到疏忽而遗漏的情况&#xff0c;同时也是非常费力耗时 比如说我们在开发环境对某个表新增了一个字段&#xff0c;而提交测试时却忘了提交该 SQL 脚本&#xff0c;导致出现 bug 而测试中断&#xf…

vs 安装 qt qt扩展 改迅雷下载qt

Qt5.14.2安装教程和VS2019中的qt环境配置-CSDN博客 1 安装qt 社区版 免费 Download Qt OSS: Get Qt Online Installer 2 vs安装 qt vs tools 3 vs添加 qt添加 bin/cmake.exe 路径 3.1 扩展 -> qt versions 3.2 4 新版要源码安装 需要自己安装 安装独立安装的旧版 官网…

go自定义端口监听停用-------解决端口被占用的问题

代码 package mainimport ("fmt""log""net""os/exec""strconv""strings" )func getSelect(beign int, end int) int {var num intfor {_, err : fmt.Scan(&num)if err ! nil {fmt.Println("输入错误&am…

【c】角谷猜想

#include<stdio.h> int coll(int x)//定义函数 {int count0;while(x>1){if(x%20){xx/2;count;}else{x3*x1;count;}}return count; } int main() {int n,num;scanf("%d",&n);int arr[n1];for(int i1;i<n;i)//输入n组数据保存到数组中{scanf("%d&…

Python图表神器:Matplotlib库绘制图表轻松有趣

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com Matplotlib是Python中用于绘制图表和数据可视化的重要库。它提供了丰富的功能和灵活性&#xff0c;可用于生成各种类型的图表&#xff0c;从简单的折线图到复杂的三维图表。 1. 基本图表绘制 折线图 Matplotl…

【【Micro Blaze 的 最后补充 与 回顾 】】

Micro Blaze 的 最后补充 与 回顾 Micro Blaze 最小系统 以 MicroBlaze 为核心、LocalMemory&#xff08;片上存储&#xff09;为内存&#xff0c;加上传输信息使用的 UART串口就构成了嵌入式最小系统。当程序比较简单时&#xff0c;Local Memory 可以作为程序的运行空间以及…

如何调用 API | 学习笔记

开发者学堂课程【阿里云 API 网关使用教程:如何调用 API】学习笔记&#xff0c;与课程紧密联系&#xff0c;让用户快速学习知识。 课程地址&#xff1a;阿里云登录 - 欢迎登录阿里云&#xff0c;安全稳定的云计算服务平台 如何调用 API 调用 API 的三要素 要调用 API 需要三…

9-3用结构体定义学生,用函数输出学生成绩

#include<stdio.h>struct student{char name[10];int num;char score[3]; }stu[5]; //结构体输入信息int main(){void print(struct student str[5]);int i,j;for(i0;i<5;i){printf("第%d个学生信息如下&#xff1a;\n",i1);printf("name:");scan…