基于Redisson实现重入锁

一. 分布式锁基础

在分布式系统中,当多个客户端(应用实例)需要访问同一资源时,可以使用分布式锁来确保同一时刻只有一个客户端能访问该资源。Redis作为高性能的内存数据库,提供了基于键值对的分布式锁实现,通常采用SETNX命令(即SET if Not eXists)来设置锁。

Redis的分布式锁大致实现流程如下:

  • 客户端请求获取锁:通过SETNX命令设置一个锁键(如lock:<resource>)。
  • 锁被设置成功时,客户端可以访问资源;否则,它会等待或返回获取失败的结果。
  • 客户端在完成工作后,释放锁:通过DEL命令删除锁键。

二. 可重入锁

在一个线程或进程中,如果该线程/进程已经获取了锁,那么它可以重复获取该锁,而不会发生死锁问题。也就是说,同一线程/进程可以多次获取锁,而每次释放锁时都需要释放一次,直到所有的锁请求都被释放。

对于分布式系统中的可重入锁,它确保:

  • 当一个客户端(通常是线程或进程)已经持有锁时,它可以继续请求该锁而不会被阻塞或进入死锁。
  • 锁的计数是线程级别的,客户端每次请求锁时都需要将锁的计数增加,释放锁时则减少计数,直到计数为零时才完全释放锁。

三. Redis实现可重入锁的步骤

1. 使用SET命令(或SETNX命令)来获取锁
  • 锁是由一个特定的Redis键(例如,lock:<resource>)表示的。
  • 锁的值通常是一个标识符,例如,客户端的UUID,或者一个线程标识符。这个值用于确保同一个客户端可以重复获取锁。
2. 记录锁的持有者
  • 当客户端请求锁时,它不仅设置一个标识该锁的键(如lock:<resource>),而且还设置该锁的值(如UUID)。这样,如果该锁已经存在且值不是当前客户端的标识符,则客户端无法获得锁。
3. 锁的可重入性
  • 在可重入锁中,客户端获取锁时,如果该客户端的标识符(如UUID)已经在锁值中,则客户端可以继续获取锁,并且需要将锁的"持有次数"增加。
  • 可以通过键值对的计数来实现这一点。每次客户端请求锁时,Redis可以增加锁的计数。当客户端释放锁时,Redis会减少锁的计数。
4. 锁的过期时间
  • 为了防止死锁,Redis的锁通常会设置一个过期时间,通常是SET命令的EX选项(设置过期时间)。这样,如果客户端在持有锁的过程中发生故障,锁会在一定时间后自动释放。
  • 可重入锁的一个实现方式是,客户端在每次获取锁时刷新过期时间,确保锁在可重入期间不会过期。
5. 释放锁
  • 当客户端完成工作后,它会释放锁。每次释放锁时,它会检查当前的锁计数:
    • 如果锁计数大于1,表示该客户端还需要继续持有锁,于是减少计数。
    • 如果锁计数为1,则客户端完全释放锁,通过删除锁键(如DEL)来释放该资源。

LUA 脚本实现简单重入锁

获取锁的 Lua 脚本:

Lua 复制代码
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间

-- 判断是否存在
if (redis.call('EXISTS', key) == 0) then
    -- 不存在,获取锁
    redis.call('HSET', key, threadId, '1');
    -- 设置有效期
    redis.call('EXPIRE', key, releaseTime);
    return 1; -- 返回结果
end;

-- 锁已经存在,判断threadId是否是自己
if (redis.call('HEXISTS', key, threadId) == 1) then
    -- 是自己,获取锁,重入次数+1
    redis.call('HINCRBY', key, threadId, '1');
    -- 设置有效期
    redis.call('EXPIRE', key, releaseTime);
    return 1; -- 返回结果
end;

return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的 Lua 脚本:

Lua 复制代码
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间

-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then
    return nil; -- 如果已经不是自己,则直接返回
end;

-- 是自己的锁,则重入次数减1
local count = redis.call('HINCRBY', key, threadId, -1);

-- 判断是否重入次数是否已经为0
if (count > 0) then
    -- 大于0说明不能释放锁,重置有效期然后返回
    redis.call('EXPIRE', key, releaseTime);
    return nil;
else
    -- 等于0说明可以释放锁,直接删除
    redis.call('DEL', key);
    return nil;
end;
相关推荐
咩咩啃树皮8 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚8 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白10 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司10 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地11 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415911 小时前
TCP超时重传机制是为了解决什么问题?
java
二宝哥12 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
莫逸风14 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
z1234567898614 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112315 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python