【Redis(9)】Spring Boot整合Redis,实现分布式锁,保证分布式系统中节点操作一致性

在上一篇系列文章中,咱们利用Redis解决了缓存穿透、缓存击穿、缓存雪崩等缓存问题,Redis除了解决缓存问题,还能干什么呢?这是今天咱们要接着探讨的问题。

在分布式系统中,为了保证在多个节点间操作的一致性,引入了分布式锁的概念。那么什么是分布式锁?为什么要用分布式锁?怎么实现分布式锁?带着这些问题,接下来本文将带你一起实现一个基于Redis的分布式锁?

什么是分布式锁?

分布式锁是一种在分布式系统中用来保证同一时间只有一个进程能操作共享资源的机制。它类似于我们熟知的单机环境下的锁,但分布式锁跨越了单机的界限,作用于多台机器之间,确保了在多个节点上的协调一致。

为什么要使用分布式锁?

在没有分布式锁的情况下,多个节点可能会同时修改共享资源,导致数据不一致甚至丢失。例如,在电子商务平台的库存管理中,如果多个用户同时下单购买同一件商品,而系统没有正确地管理库存,就可能出现超卖的情况。

如何实现分布式锁?

实现分布式锁有多种方式,以下是一些常见的实现策略:

基于数据库的锁:使用数据库的排他锁(如SQL中的SELECT ... FOR UPDATE)可以实现简单的分布式锁。

基于缓存的锁:使用分布式缓存系统(如Redis)提供的原子命令(如SETNX)来实现锁的功能。

基于ZooKeeper的锁:ZooKeeper的临时有序节点可以用来实现分布式锁,通过节点的创建和监听来实现锁的获取和释放。

基于etcd的锁:etcd是一个分布式键值存储,也常被用来实现分布式锁,它提供了可靠的键值存储和原子操作。

Redis实现分布式锁

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;/*** 分布式锁实现,使用Redis作为后端存储。*/
@Component
public class RedisDistributedLock {// 从配置文件中读取锁的默认有效期(毫秒)@Value("${lock.leaseTime:30000}")private long leaseTime;private final StringRedisTemplate stringRedisTemplate;private final RedisScript lockScript;private final RedisScript unlockScript;private final ReentrantLock lockReentrantLock = new ReentrantLock();// 存储锁的持有者信息private final Map<String, String> locks = new ConcurrentHashMap<>();// 存储锁的过期时间private final Map<String, Long> lockExpirationTimes = new ConcurrentHashMap<>();// 锁重试间隔时间private static final long LOCK_RETRY_INTERVAL_MS = 100L;// 锁最大重试次数private static final long LOCK_MAX_RETRY_TIMES = 10L;@Autowiredpublic RedisDistributedLock(StringRedisTemplate stringRedisTemplate, ResourceLoader resourceLoader) throws IOException {this.stringRedisTemplate = stringRedisTemplate;// 加载和编译Lua锁脚本this.lockScript = loadScript(resourceLoader, "lock.lua");this.unlockScript = loadScript(resourceLoader, "unlock.lua");}/*** 从指定路径加载Lua脚本。*/private RedisScript loadScript(ResourceLoader resourceLoader, String scriptPath) throws IOException {Resource resource = resourceLoader.getResource("classpath:" + scriptPath);String script = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);return stringRedisTemplate.getConnectionFactory().getConnection().scriptLoad(script);}/*** 尝试获取锁。** @param lockKey 锁的键。* @param waitTime 锁的最长等待时间。* @return 如果获取到锁,返回true;否则返回false。*/public boolean tryLock(String lockKey, long waitTime) {String requestId = UUID.randomUUID().toString();long endTime = System.currentTimeMillis() + waitTime;int attempts = 0;while (System.currentTimeMillis() < endTime && attempts < LOCK_MAX_RETRY_TIMES) {if (tryLockInner(lockKey, requestId)) {lockReentrantLock.lock();try {// 记录锁信息和过期时间locks.put(lockKey, requestId);lockExpirationTimes.put(lockKey, System.currentTimeMillis() + leaseTime);return true;} finally {lockReentrantLock.unlock();}}attempts++;try {Thread.sleep(LOCK_RETRY_INTERVAL_MS);} catch (InterruptedException e) {Thread.currentThread().interrupt();// 如果线程被中断,返回falsereturn false;}}return false;}/*** 尝试获取锁的内部方法,使用Redis Lua脚本以保证原子性。*/private boolean tryLockInner(String lockKey, String requestId) {return (Boolean) stringRedisTemplate.execute(lockScript,Collections.singletonList(lockKey),requestId,TimeUnit.MILLISECONDS.toMillis(leaseTime));}/*** 释放锁。** @param lockKey 锁的键。*/public void unlock(String lockKey) {lockReentrantLock.lock();try {String requestId = locks.remove(lockKey);if (requestId != null) {unlockInner(lockKey, requestId);lockExpirationTimes.remove(lockKey);}} finally {lockReentrantLock.unlock();}}/*** 释放锁的内部方法,使用Redis Lua脚本以保证原子性。*/private boolean unlockInner(String lockKey, String requestId) {return (Long) stringRedisTemplate.execute(unlockScript,Collections.singletonList(lockKey),requestId) == 1;}/*** 定时任务,用于续期已获取的锁。*/@Scheduled(fixedRateString = "${lock.renewRate:1000}")public void renewLocks() {lockReentrantLock.lock();try {long now = System.currentTimeMillis();for (Map.Entry<String, Long> entry : lockExpirationTimes.entrySet()) {if (now > entry.getValue()) {// 如果锁已过期,释放锁unlock(entry.getKey());} else {// 续期锁boolean renewed = stringRedisTemplate.expire(entry.getKey(), leaseTime - (now - entry.getValue()), TimeUnit.MILLISECONDS);if (!renewed) {// 如果续期失败,释放锁unlock(entry.getKey());}}}} catch (Exception e) {// 记录异常日志} finally {lockReentrantLock.unlock();}}
}

请注意以下几点:

  1. 为了加载Lua脚本,这里使用了Spring的ResourceLoader。需要在类路径下提供lock.luaunlock.lua文件。

  2. tryLock方法只接受锁的键和等待时间,leaseTime从配置文件中获取。

  3. unlock方法只负责解锁,不从等待队列中移除。

  4. renewLocks方法会检查每个锁是否过期,并相应地续期或释放。

  5. 使用了ConcurrentHashMap来存储锁信息和锁过期时间,以支持高并发。

  6. 添加了异常处理和中断处理,但没有实现日志记录。需要根据日志框架(如SLF4J、Log4J等)添加适当的日志记录。

  7. 请确保的配置文件(如application.properties)中设置了lock.leaseTimelock.renewRate属性。

lock.lua文件

-- lock.lua
-- 参数1: 锁的key
-- 参数2: 请求ID
-- 参数3: 锁的超时时间(毫秒)local lockKey = KEYS[1]
local requestId = ARGV[1]
local leaseTime = tonumber(ARGV[2])-- 检查锁是否存在,如果不存在则设置锁,并返回1
if redis.call('set', lockKey, requestId, 'NX', 'PX', leaseTime) == 1 thenreturn 1
else-- 如果锁已经存在,则返回0return 0
end

unlock.lua文件

-- unlock.lua
-- 参数1: 锁的key
-- 参数2: 请求IDlocal lockKey = KEYS[1]
local requestId = ARGV[1]-- 检查锁是否存在,并且锁的持有者ID与传入的请求ID匹配,如果匹配则删除锁
if redis.call('get', lockKey) == requestId thenreturn redis.call('del', lockKey)
else-- 如果锁存在但请求ID不匹配,或者锁不存在,则返回0return 0
end

这些Lua脚本通过使用Redis的原子命令来确保锁的获取和释放操作的原子性。在lock.lua脚本中,使用set命令尝试设置一个锁,如果锁不存在(NX),则设置成功并返回1,否则返回0。

unlock.lua脚本中,首先检查锁是否存在,并且当前请求者是否是锁的持有者,如果是,则删除锁。

请将这些脚本保存为.lua文件,并确保它们位于Spring Boot项目的classpath路径下,以便RedisDistributedLock类可以加载它们。同时,确保Redis服务器配置允许执行Lua脚本,并且没有禁用Lua脚本命令。

使用方式

首先,确保您的项目中已经包含了Spring框架和Spring Data Redis的相关依赖。然后,按照以下步骤使用上述代码:

  1. 配置Redis:在Spring配置文件中配置Redis连接信息。

  2. 注入依赖:在Spring组件中注入StringRedisTemplateResourceLoader

  3. 配置锁参数:在配置文件中设置锁的有效期(lock.leaseTime)和锁续期的时间间隔(lock.renewRate)。

  4. 使用锁:在需要同步的代码块前后,使用tryLock方法尝试获取锁,并在操作完成后调用unlock方法释放锁。

    @Autowired
    private RedisDistributedLock redisDistributedLock;public void criticalSection() {String lockKey = "some_resource_key";long waitTime = 10000; // 等待10秒获取锁if (redisDistributedLock.tryLock(lockKey, waitTime)) {try {// 临界区代码} finally {redisDistributedLock.unlock(lockKey);}}
    }

优缺点

优点

  • 线程安全:通过ReentrantLock确保了多线程环境下的线程安全。
  • 自动续期:通过定时任务自动续期,减少了锁提前释放的风险。
  • 高可用:Redis的分布式特性提供了高可用的锁机制。

缺点

  • 资源消耗:定时任务和锁重试机制可能会增加系统资源的消耗。
  • 复杂性:引入了额外的Lua脚本和锁管理逻辑,增加了系统的复杂性。

改进点

异常处理:增强异常处理,确保在出现异常时能够记录日志并采取适当的恢复措施。

性能监控:引入性能监控,以便及时发现并解决潜在的性能瓶颈。

锁优化:考虑使用更高效的锁重试策略,如指数退避,以减少资源消耗。

注意事项

版本兼容性:确保Redis和Spring Data Redis的版本兼容。

锁超时设置:合理设置锁的有效期,避免死锁或资源浪费。

资源释放:确保在操作完成后释放锁,避免资源长时间被占用。

使用场景案例

场景一:数据库记录更新

在多实例的微服务架构中,当需要更新共享数据库中的记录时,可以使用分布式锁来保证同一时间只有一个实例进行更新。

if (redisDistributedLock.tryLock("db_record_123", 5000)) {try {// 更新数据库记录} finally {redisDistributedLock.unlock("db_record_123");}
}

场景二:分布式任务调度

在分布式任务调度系统中,使用分布式锁可以避免同一个任务被多个实例重复执行。

if (redisDistributedLock.tryLock("task_123", 5000)) {try {// 执行任务} finally {redisDistributedLock.unlock("task_123");}
}

场景三:分布式缓存更新

当多个服务实例需要更新同一个缓存项时,使用分布式锁可以保证只有一个实例在任何给定时间更新缓存。

if (redisDistributedLock.tryLock("cache_item_123", 5000)) {try {// 更新缓存项} finally {redisDistributedLock.unlock("cache_item_123");}
}

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

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

相关文章

系统安全与应用(1)

目录 1、账号安全管理 &#xff08;1&#xff09;禁止程序用户登录 &#xff08;2&#xff09;锁定禁用长期不使用的用户 &#xff08;3&#xff09;删除无用的账号 &#xff08;4&#xff09;禁止账号和密码的修改 2、密码安全管理 设置密码有效期 1&#xff09;针对已…

Centos7 tcpdump -w 时遇到 Permission denied

一、问题 使用tcpdump抓包并写入文件时出现 Permission denied&#xff0c;权限不足。 [rootstorm03 tcpdumpTest]# tcpdump -i em4 udp and host 225.1.2.5 and port 10111 -G 60 -w %Y_%m%d_%H%M_%S.pcap tcpdump: listening on em4, link-type EN10MB (Ethernet), capture…

oracle之--动态sql(execute immediate ‘ ‘)

动态sql--execute immediate 原因&#xff1a;ddl语句&#xff0c;truncate语句 不能直接使用&#xff0c;需要封装起来 --动态sql--execute immediate 因为ddl&#xff0c;truncate 不能直接使用&#xff0c;需要封装起来 --1.TRUNCATE table declare BEGIN --truncate…

熵权法处理TIFF图像

一、熵权法 又称熵值法&#xff0c;是一种客观赋权法&#xff0c;根据各项指标观测值所提供的信息大小来确定指标权重&#xff0c;具体细节可以参阅Stata-熵值法&#xff08;熵权法&#xff09;计算实现。 二、原理 根据指标特性&#xff0c;可以用熵值判断某个指标的离散程…

40、排列数字

排列数字 题目描述 给定一个整数n&#xff0c;将数字1~n排成一排&#xff0c;将会有很多种排列方法。 现在&#xff0c;请你按照字典序将所有的排列方法输出。 输入格式 共一行&#xff0c;包含一个整数n。 输出格式 按字典序输出所有排列方案&#xff0c;每个方案占一行…

一句话或一张图讲清楚系列之——ISERDESE2的原理

主要参考&#xff1a; https://blog.csdn.net/weixin_50810761/article/details/137383681 xilinx原语详解及仿真——ISERDESE2 作者&#xff1a;电路_fpga https://blog.csdn.net/weixin_45372778/article/details/122036112 Xilinx ISERDESE2应用笔记及仿真实操 作者&#x…

K8S Prometheus Springboot Actuator ServiceMonitor配置

用于展示Springboot Actuator监控内容 引入Springboot相关的监控配置包 Springboot pom配置 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><depende…

前端CSS基础7(背景相关属性,鼠标相关属性)

前端CSS基础7&#xff08;元素的背景相关属性&#xff0c;鼠标相关属性&#xff09; CSS背景相关属性CSS鼠标相关属性 CSS背景相关属性 在 CSS 中&#xff0c;可以使用多种属性来设置元素的背景样式。以下是一些常用的 CSS 背景相关属性&#xff1a; background-color&#x…

K8s: Ingress对象, 创建Ingress控制器, 创建Ingress资源并暴露服务

Ingress对象 1 &#xff09;概述 Ingress 是对集群中服务的外部访问进行管理的 API 对象&#xff0c;典型的访问方式是 HTTPIngress-nginx 本质是网关&#xff0c;当你请求 abc.com/service/a, Ingress 就把对应的地址转发给你&#xff0c;底层运行了一个 nginx但 K8s 为什么不…

F5应用及配置

F5网络公司的BIG-IP系列设备主要被应用于负载均衡&#xff0c;同时也提供应用交付网络功能。 以下是F5 BIG-IP配置和应用的一些要点&#xff1a; 管理接口&#xff1a;F5设备可以通过图形化界面或命令行界面进行配置和管理。图形化界面适合进行设备的基础以及高级调试&#x…

framework.jar如何导入到android studio中进行framework的开发+系统签名

framework的开发 生成framework.jar的方式 链接: framework.jar 生成 如何生成一个系统签名 链接: 生产系统签名 生成 platform.x509.pem、platform.pk8文件位置 生产系统签名 清单文件位置改变 <manifest xmlns:android"http://schemas.android.com/apk/res/a…

代码随想录算法训练营第6天 | 242. 有效的字母异位词 | 349. 两个数组的交集 | 202. 快乐数 | 1. 两数之和

242. 有效的字母异位词 题意 两个字符串中每个字符的出现次数是否一样 解 hash bool isAnagram(char* s, char* t) {int array[30];memset(array, 0, sizeof(int) * 30);for (int i 0; s[i] ! \0; i) {array[s[i] - a];}for (int i 0; t[i] ! \0; i) {array[t[i]-a]--;}…

modelsim波形高度异常,值为X

一、问题 波形高度异常&#xff0c;忽高忽低&#xff0c;正常波形高电平和低电平是统一高度的 timescale 1ns/1nsmodule key_test_tb();//parameter define parameter CLK_PERIOD 20; parameter CNT_MAX 25d25; //仅用于仿真,对应 500nsreg sys_clk; //周期 20ns reg d; wir…

刷代码随想录有感(43):遍历N叉树

题干&#xff1a;N叉树的前序遍历、后序遍历、层序遍历。 代码&#xff1a; class Node{//前序遍历N叉树&#xff08;递归实现&#xff09; public:int val;vector<Node*>children;Node(int _val, vector<Node*>_children): val(_val), children(_children){} };…

13.接口自动化学习-Pytest结合Yaml使用

问题&#xff1a;项目自动化测试脚本迭代出现变革技术方案 要求&#xff1a;测试用例从excel–变为yaml用例 注意事项&#xff1a; 1&#xff09;尽可能少改代码 2&#xff09;新技术方案yaml读取&#xff0c;尽可能写成一样的数据返回 [(请求体1,响应数据1),(请求体2,响应数据…

AR模块中通用对账的优化尝试

背景&#xff1a; 用户在唯品会下单后&#xff0c;是可以自由选择不同支付方式进行支付的&#xff0c;支付后&#xff0c;支付系统会将一笔收款单传送给AR&#xff0c;AR财务可以从此处看到收款情况。但是&#xff0c;真实的资金是按照不同支付方式&#xff0c;由银行或者其他渠…

ffmpeg初体验

一&#xff1a;安装 sudo yum install epel-release -y sudo yum update -ysudo rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpmyum -y install …

在 Oracle 数据库中使用正则表达式

在 Oracle 数据库中使用正则表达式 0. 引言1. 什么是正则表达式&#xff1f;2. Oracle 数据库正则表达式支持3. 用于正则表达式的 Oracle 数据库 SQL 函数4. 正则表达式中支持的元字符5. 构建正则表达式 0. 引言 本文介绍 Oracle 数据库的正则表达式支持。本文涵盖以下主题&am…

Unity构建详解(10)——Unity构建流程

【前言】 我们知道从源代码到可执行文件有四个步骤&#xff1a;预编译、编译、汇编、链接 预编译&#xff1a;处理源代码文件中的以“#”开始的各种预编译指令编译&#xff1a;通过语法语义分析等将源代码文件转为中间语言文件并进行优化&#xff0c;再生成汇编代码文件汇编&…

Vs Code npm install 报错解决方法

用的人家的前端框架发现是封装过的&#xff0c;要修改人家前端的话还得把前端源码放在Vs Code 上运行&#xff0c;后端放在IDEA上运行&#xff0c;然后前后端并行开发&#xff0c;在配置前端环境时遇到&#xff1a; npm install 这个的原因是我把node下载到D盘了权限不够框框爆…