基于 SpringBoot + Redis 实现分布式锁

news/2025/3/16 3:13:27/

大家好,我是余数,这两天温习了下分布式锁,然后就顺便整理了这篇文章出来。文末附有源码链接,需要的朋友可以自取。

至于什么是分布式锁,这里不做赘述,不了解的可以自行去查阅资料。

文章目录

    • 实现要点
    • 项目结构
    • Parent Maven依赖
    • 锁的定义
    • 锁的使用
    • 源码地址
    • 参考资料
      • 如何用Redis实现分布式锁

实现要点

1. 使用 Redis 的 Setnx(SET if Not Exists) 命令加锁。

即锁不存在的时候才能加锁成功。如果锁存在了,说明其他服务已经持有该锁了,所以加锁失败。

2. 需要设置过期时间。

防止持有锁的服务意外挂掉后无法释放锁,导致其他服务永远都获取不到锁。

3. 锁的名字是固定的,但是锁的值需要保证线程唯一。

防止误删其他服务(线程)持有的锁。比如 线程A 获取到锁后被挂起了,等到锁自动过期后 线程B 又获得了锁,然后 线程B 开始执行自己的业务逻辑。

这个时候如果 线程A 被唤醒后并执行完所有的业务逻辑需要释放锁了,但这个时锁的持有者其实是 线程B,如果只根据 key 去释放锁的话,那么 线程A 就错误的把 线程B 持有的锁给释放掉了。

所以我们需要让 线程A 释放锁的时候,先判断一下锁是不是自己持有的,是才能释放。

判断锁是不是自己持有的就是通过加锁时给锁设置的 value来确定的。

4. 使用 Lua 脚本保证 “判断是否是自己持有的锁” 和 “释放锁” 的原子性。

因为判断和释放是两个命令,如果不保证原子性,会出现这种情况:刚判断完这个锁是自己的,然后这个锁就过期且被其他服务获取到了,你再释放岂不是把其他服务的锁给释放掉了。

5. 动态刷新锁的过期时间。

如果业务逻辑比较耗时,还没执行完锁就过期了怎么办。因为过期时间是在加锁的时候设置的,根本没有办法准确的预估到业务究竟需要多长时间。所以我们需要在业务逻辑没执行完的时候动态给锁续期,也就是更新锁的过期时间。

项目结构

  1. FileService: 模拟共享资源操作,以及分布式锁的实现。
  2. ServiceA:模拟分布式服务,读写共享资源。
  3. ServiceB:模拟分布式服务,读写共享资源。
  4. CountFile:共享资源,服务A服务B 会读写该文件中的内容。

Parent Maven依赖

基于SpringBoot 3.0.6。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>demo.iyushu</groupId><artifactId>redis-lock</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><modules><module>ServiceA</module><module>ServiceB</module><module>FileService</module></modules><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.0.6</version><relativePath/></parent><properties><maven.compiler.source>19</maven.compiler.source><maven.compiler.target>19</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties></project>

锁的定义

锁的定义是在 FileService 模块实现的,同时该模块还实现了文件读写操作,结构如下:

  1. FileService:读写文件服务。
  2. Lock:定义一个锁。
public class Lock{private String key;private String value;// unit is secondprivate int timeout;// 看门狗watchDog,用于给锁续期。private LockService.WatchDog watchDog;
}
  1. LockService:加锁和释放锁。

加锁:加锁时需要指定加锁的 keyvalue,和超时时间timeout。等待20s没有获取到锁则认为加锁失败。

加锁成功则返回锁,加锁失败返回空。

    public Lock lock(String key, String value, int timeout){boolean success = false;long start = System.currentTimeMillis();// 加锁等待时间 20swhile(!success && System.currentTimeMillis() - start <= 20000){success =  redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);}if(success){Lock lock = new Lock(key, value, timeout, new WatchDog());lock.getWatchDog().start();return lock;}return null;}

释放锁:先判断是自己的锁,然后将锁删掉,需要使用Lua脚本确认这两步的原子性。同时停止给锁的续期。

    public void unlock(Lock lock){redisTemplate.execute(unlockScript, Arrays.asList(lock.getKey()), lock.getValue());lock.getWatchDog().stop();}

Lua 脚本

--- Lua脚本语言,释放锁 比较value是否相等,避免删除别人占有的锁
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

动态给锁续期:加锁成功后会新启一个线程,每隔1s给锁续期一次,直到业务逻辑执行完并释放锁后,续期线程中断。

    public class WatchDog implements Runnable {private Lock lock;public void setLock(Lock lock) {this.lock = lock;}private Thread thread;public void start(){thread = new Thread(this);thread.start();}public void stop(){thread.interrupt();}@Overridepublic void run() {long start = System.currentTimeMillis();while(!thread.isInterrupted()){long current = System.currentTimeMillis();if(current - start >= 1000){System.out.println("续期。。。");redisTemplate.opsForValue().getAndExpire(lock.getKey(), lock.getTimeout(), TimeUnit.SECONDS);start = current;}}}}

锁的使用

ServiceAServieB 主要模拟了实际业务中的多个服务,执行有资源共享的业务逻辑前先获取锁,获取成功才继续执行,执行完成后释放锁。

注意加锁的时候,value 值需要是唯一的,这里使用了服务名 + 线程名的方式。

public int getCount() throws InterruptedException {Lock lock = null;try{// 获取锁lock = lockService.lock("lock", "服务A" + Thread.currentThread().getName(), 3);if(lock == null){throw new RuntimeException("lock failed");}int count = fileService.getCount();System.out.println("服务A获取到的计数为" + count);// 模拟业务逻辑Thread.sleep(2000);fileService.setCount(count + 1);return count;}finally {// 释放锁if(lock != null){lockService.unlock(lock);}}
}

我们让 服务A服务B 同时各执行10次读写文件的操作,每次写的时候将文件中的数字加一,看看结果如何。

服务A运行结果:

服务B运行结果:

可以看到加锁成功,计数是正常的。两个服务几乎是交替执行的,即一个服务执行完成后,另一个服务才能获取到锁并执行业务逻辑。

那试试不加锁会怎样呢?对比一下看看,将 服务A服务B 中加锁的逻辑注释掉。

毫无悬念的,服务B服务A 的计数完全覆盖了。

源码地址

以下是源码链接,仅用于理解Redis分布式锁的实现原理,代码还有很多不足,切勿用于生产环境哟,欢迎多多交流,嘿嘿~

https://github.com/justyuze/share-redis-lock/tree/main

参考资料

如何用Redis实现分布式锁

https://blog.csdn.net/fuzhongmin05/article/details/119251590


http://www.ppmy.cn/news/92829.html

相关文章

SVN安装教程详解:快速掌握SVN的安装和使用方法

本文为大家介绍了SVN的详细安装步骤&#xff0c;并提供了具体的命令行操作和TortoiseSVN的图形用户界面操作说明&#xff0c;帮助读者轻松掌握SVN的安装和使用方法。包括创建版本库&#xff0c;添加项目文件到版本库&#xff0c;创建工作副本&#xff0c;对文件进行修改和提交&…

[GreyCTF‘23] crypto部分

baby crypto 凯撒签到 whuo{squi4h_s1fx3h_v0h_co_i4b4T} grey{caes4r_c1ph3r_f0r_my_s4l4D} The Vault 这里只有一个check_keys函数&#xff0c;加密这块破不了&#xff0c;只要过了check_keys就行。 from hashlib import sha256 from Crypto.Util.number import long_to_…

Linux网络服务:部署YUM仓库与NFS服务

目录 一、理论 1.部署YUM仓库服务 2.NFS共享存储服务 二、实验 1.通过httpd服务建立yum仓库 2.通过vsftpd服务建立yum仓库 3.搭建NFS实现2台或3台服务器共享一个目录 一、理论 1.部署YUM仓库服务 (1) YUM简介 YUM的前身是YUP&#xff0c;借助于YUM软件仓库&#xff0c…

如何使用Chatgpt做论文降重呢?

使用ChatGPT做论文降重详细操作步骤 一、说明 1、普通的降重方法有&#xff1a;多重翻译降重、改写润色降重、续写降重&#xff0c;在降重的过程中可以配合使用&#xff0c;效果更加。 2、ChatGPT的高级降重方法在最后一个&#xff0c;就是dan模式降重&#xff0c;dan可以自定义…

Nacos配置管理、Fegin远程调用、Gateway服务网关

1.Nacos配置管理 Nacos除了可以做注册中心&#xff0c;同样可以做配置管理来使用。 1.1.统一配置管理 当微服务部署的实例越来越多&#xff0c;达到数十、数百时&#xff0c;逐个修改微服务配置就会让人抓狂&#xff0c;而且很容易出错。我们需要一种统一配置管理方案&#xf…

014、检查点

检查点 检查点触发机制为什么需要检查点检查点工作流程检查点记录位置Database Recovery检查点调整checkpoint_completion_target检查点触发机制 在PostgreSQL中,检查点(后台)进程执行检查点;当发生下列情况之一时,其进程将启动: 检查点间隔时间由checkpoint_timeout设置…

JAVA创建线程的两种方法

Java创建线程的方法有两种 两种方法分别是继承Thread类和实现Runnable接口。 继承Thread类 class MyThread extends Thread {Overridepublic void run() {System.out.println("Hello World!");} }public class Main {public static void main(String[] args) {MyT…

SQLite 数据库入门教程(GO)

文章目录 SQLite数据库入门教程一、SQLite 简介1、什么是 SQLite&#xff1f;2、为什么要用 SQLite&#xff1f; 二、SQLite 安装1、在 Windows 上安装 SQLite2、在 Linux 、Mac OS上安装 SQLite 三、SQLite 命令四、SQLite 使用1、SQLite 数据类型2、SQLite 语法3、SQLite 可视…