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

ops/2025/2/14 4:05:55/

在上一篇系列文章中,咱们利用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.ppmy.cn/ops/8969.html

相关文章

Proxyman Premium for Mac:网络调试利器,开发者首选!

Proxyman Premium for Mac是一款功能强大的网络调试和分析工具&#xff0c;专为开发者和测试人员打造。这款软件以其出色的性能和丰富的功能&#xff0c;帮助用户在网络开发和调试过程中更有效地分析和拦截网络请求&#xff0c;进行必要的修改和重发&#xff0c;从而进行更深度…

Shell和Linux权限

目录 shell Liunx权限 用户 sudo Linux的权限管理 文件访问者的分类 文件的属性 文件的权限 文件全权限值的表示方法 1.字符表示 2.八进制数值表示 用户符号 修改文件访问权限 修改文件拥有者 修改拥有者和所属组 修改所属组 文件目录的权限的含义 问题 粘滞…

Scala详解(4)

Scala 面向对象 案例 定义类表示矩形(Rectangle)&#xff0c;提供对外获取周长(girth)和面积(area)的函数&#xff0c;并且为这个矩形类提供一个子类表示正方形(Square) package com.fesco.exer ​ object ObjectDemo1 { ​def main(args: Array[String]): Unit { ​val r …

37. UE5 RPG创建自定义的Ability Task

在前面的文章中&#xff0c;我们实现了一个火球术的一些基本功能&#xff0c;火球术技能的释放&#xff0c;在技能释放后&#xff0c;播放释放动画&#xff0c;在动画播放到需要释放火球术的位置时&#xff0c;将触发动画通知&#xff0c;在动画通知中触发标签事件&#xff0c;…

解析溅射:通过解析积分进行抗混叠的3D高斯溅射

Analytic-Splatting: Anti-Aliased 3D Gaussian Splatting via Analytic Integration 解析溅射&#xff1a;通过解析积分进行抗混叠的3D高斯溅射 Zhihao Liang 梁志豪11**Qi Zhang 琦张22††Wenbo Hu 胡文博22Lei Zhu 雷竹33Ying Feng 英凤22Kui Jia 贾奎44†† Abstrac…

使用FPGA实现超前进位加法器

介绍 前面已经向大家介绍过8位逐位进位加法器了&#xff0c;今天向大家介绍4位超前进位加法器。 对于逐位进位加法器来说&#xff0c;计算任意一位的加法运算时&#xff0c;必须等到低位的加法运算结束送来进位才能运行。这种加法器结构简单&#xff0c;但是运算慢。 对于超…

【Spring】依赖注入(DI)时常用的注解@Autowired和@Value

目录 1、Autowired 自动装配 1.1、要实现自动装配不是一定要使用Autowired 1.2、Autowired的特性 &#xff08;1&#xff09;首先会根据类型去spring容器中找(bytype),如果有多个类型&#xff0c;会根据名字再去spring容器中找(byname) &#xff08;2&#xff09;如果根据名…

Linux的学习之路:12、地址空间(续)与进程的创建、终止和等待

摘要 本章将讲述上章没说的一些东西以及进程创建终止与等待 目录 摘要 一、地址空间&#xff08;续&#xff09; 二、创建 三、终止 四、等待 五、思维导图 一、地址空间&#xff08;续&#xff09; 上篇文章中介绍了地址空间&#xff0c;但是没有说为什么会有地址空间…