可直接落地的 Redis 分布式锁实现:包含最小可用版、生产可用版(带 Lua 原子解锁、续期“看门狗”、自旋等待、可重入)、以及基于注解+AOP 的无侵入用法,最后还给出 Redisson 方案对比与踩坑清单。


一、设计目标与约束

  • 获取锁:SET key value NX PX ttl(原子、带过期)
  • 释放锁:Lua 校验 value(token)后再 DEL,避免误删他人锁
  • 等待策略:可设置总体等待时长 + 抖动退避,避免惊群
  • 续期(看门狗):长耗时任务自动延长锁过期,避免任务未完成锁先过期
  • 可重入:同一线程/请求二次进入同一锁,计数 +1,退出时计数 -1
  • 可观测性:日志、指标(命中/失败/续期次数等)

二、最小可用实现(入门示例)

// MinimalLockService.java
@Service
public class MinimalLockService {private final StringRedisTemplate redis;public MinimalLockService(StringRedisTemplate redis) {this.redis = redis;}/** 获取锁,返回 token(uuid),失败返回 null */public String tryLock(String key, long ttlMs) {String token = UUID.randomUUID().toString();Boolean ok = redis.opsForValue().setIfAbsent(key, token, Duration.ofMillis(ttlMs));return Boolean.TRUE.equals(ok) ? token : null;}/** 释放锁(Lua):只有持有相同 token 才能删除锁 */public boolean unlock(String key, String token) {String script = """if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('del', KEYS[1])elsereturn 0end""";Long res = redis.execute(new DefaultRedisScript<>(script, Long.class), List.of(key), token);return res != null && res > 0;}
}

适合“单次短任务、不等待”的场景;生产建议使用下文增强版。


三、生产可用锁客户端(可重入 + 等待 + 续期)

1)核心实现

// RedisDistributedLock.java
@Component
public class RedisDistributedLock implements InitializingBean, DisposableBean {private final StringRedisTemplate redis;private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);private final DefaultRedisScript<Long> unlockScript = new DefaultRedisScript<>();private final DefaultRedisScript<Long> renewScript  = new DefaultRedisScript<>();// 线程内可重入计数:key -> (token, count)private final ThreadLocal<Map<String, ReentryState>> reentry = ThreadLocal.withInitial(HashMap::new);public RedisDistributedLock(StringRedisTemplate redis) {this.redis = redis;}@Override public void afterPropertiesSet() {unlockScript.setResultType(Long.class);unlockScript.setScriptText("""if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('del', KEYS[1])elsereturn 0end""");renewScript.setResultType(Long.class);renewScript.setScriptText("""if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('pexpire', KEYS[1], ARGV[2])elsereturn 0end""");}@Override public void destroy() { scheduler.shutdownNow(); }public static class LockHandle implements AutoCloseable {private final RedisDistributedLock client;private final String key;private final String token;private final long ttlMs;private final ScheduledFuture<?> watchdogTask;private boolean closed = false;private LockHandle(RedisDistributedLock c, String key, String token, long ttlMs, ScheduledFuture<?> task) {this.client = c; this.key = key; this.token = token; this.ttlMs = ttlMs; this.watchdogTask = task;}@Override public void close() {if (closed) return;closed = true;if (watchdogTask != null) watchdogTask.cancel(false);client.release(key, token);}public String key() { return key; }public String token() { return token; }}private record ReentryState(String token, AtomicInteger count) {}/** 尝试在 waitMs 内获取锁;持有 ttlMs;支持可重入与退避等待;启用自动续期(watchdog=true) */public Optional<LockHandle> acquire(String key, long ttlMs, long waitMs, boolean watchdog) {Map<String, ReentryState> map = reentry.get();// 可重入:当前线程已持有同一 keyif (map.containsKey(key)) {map.get(key).count().incrementAndGet();return Optional.of(new LockHandle(this, key, map.get(key).token(), ttlMs, null));}final String token = UUID.randomUUID().toString();final long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(waitMs);while (true) {Boolean ok = redis.opsForValue().setIfAbsent(key, token, Duration.ofMillis(ttlMs));if (Boolean.TRUE.equals(ok)) {map.put(key, new ReentryState(token, new AtomicInteger(1)));ScheduledFuture<?> task = null;if (watchdog) {// 续期间隔:ttl 的 1/2(保守 <= 2/3 均可)long interval = Math.max(500, ttlMs / 2);task = scheduler.scheduleAtFixedRate(() -> renew(key, token, ttlMs),interval, interval, TimeUnit.MILLISECONDS);}return Optional.of(new LockHandle(this, key, token, ttlMs, task));}if (System.nanoTime() > deadline) break;// 抖动退避:50~150mstry { Thread.sleep(50 + ThreadLocalRandom.current().nextInt(100)); }catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; }}return Optional.empty();}private void renew(String key, String token, long ttlMs) {try {Long r = redis.execute(renewScript, List.of(key), token, String.valueOf(ttlMs));// 失败说明锁已不在或被他人占有,停止续期} catch (Exception ignore) {}}private void release(String key, String token) {Map<String, ReentryState> map = reentry.get();ReentryState state = map.get(key);if (state == null) return; // 非当前线程,无操作(幂等)if (state.count().decrementAndGet() > 0) return; // 仍有重入层级map.remove(key);try {redis.execute(unlockScript, List.of(key), token);} catch (Exception e) {// 记录日志/指标}}
}

2)使用范例(try-with-resources 自动释放)

@Service
public class OrderService {private final RedisDistributedLock lock;public OrderService(RedisDistributedLock lock) { this.lock = lock; }public void deductStock(String skuId) {String key = "lock:stock:" + skuId;Optional<RedisDistributedLock.LockHandle> h =lock.acquire(key, /*ttlMs*/ 10_000, /*waitMs*/ 3_000, /*watchdog*/ true);if (h.isEmpty()) {throw new IllegalStateException("系统繁忙,请稍后重试");}try (RedisDistributedLock.LockHandle ignored = h.get()) {// 业务逻辑:查询库存 -> 校验 -> 扣减 -> 持久化// ...(这里可再次重入同锁,例如调用内部方法)}}
}

四、注解 + AOP:无侵入加锁(支持 SpEL 动态 key)

1)定义注解

// RedisLock.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {/** 锁名(前缀) */String name();/** 业务 key 的 SpEL,例如 "#skuId" 或 "#req.userId + ':' + #req.orderId" */String key();/** 过期毫秒 */long ttlMs() default 10_000;/** 最长等待毫秒 */long waitMs() default 3_000;/** 是否自动续期 */boolean watchdog() default true;/** 获取失败是否抛异常;false 则直接跳过执行业务 */boolean failFast() default true;
}

2)AOP 切面

// RedisLockAspect.java
@Aspect
@Component
public class RedisLockAspect {private final RedisDistributedLock locker;private final SpelExpressionParser parser = new SpelExpressionParser();private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();public RedisLockAspect(RedisDistributedLock locker) { this.locker = locker; }@Around("@annotation(anno)")public Object around(ProceedingJoinPoint pjp, RedisLock anno) throws Throwable {MethodSignature sig = (MethodSignature) pjp.getSignature();Method method = sig.getMethod();String spel = anno.key();EvaluationContext ctx = new StandardEvaluationContext();String[] paramNames = nameDiscoverer.getParameterNames(method);Object[] args = pjp.getArgs();if (paramNames != null) {for (int i = 0; i < paramNames.length; i++) {ctx.setVariable(paramNames[i], args[i]);}}String bizKey = parser.parseExpression(spel).getValue(ctx, String.class);String lockKey = "lock:" + anno.name() + ":" + bizKey;Optional<RedisDistributedLock.LockHandle> h =locker.acquire(lockKey, anno.ttlMs(), anno.waitMs(), anno.watchdog());if (h.isEmpty()) {if (anno.failFast()) {throw new IllegalStateException("并发过高,稍后再试");} else {return null; // 或者返回自定义“占用中”结果}}try (RedisDistributedLock.LockHandle ignored = h.get()) {return pjp.proceed();}}
}

3)业务使用

@Service
public class CheckoutService {@RedisLock(name = "pay", key = "#orderId", ttlMs = 15000, waitMs = 5000)public String pay(Long orderId) {// 幂等校验、扣款、记账、改状态...return "OK";}
}

五、和 Redisson 的取舍

  • 自己实现(本文方案)
    轻量、可控、无第三方依赖;需要你自己维护续期、统计、容错。
  • Redisson
    功能齐全(公平锁、信号量、读写锁、锁续期看门狗、联锁/红锁等),配置简单,实战成熟。
    👉 建议对锁模型复杂、需要多数据结构协作的场景直接上 Redisson。

示例(Redisson):

<!-- pom -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.38.0</version>
</dependency>
@Autowired private RedissonClient redisson;public void doWork() {RLock lock = redisson.getLock("lock:demo");// 默认看门狗 30s,自动续期if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {try { /* 业务 */ }finally { lock.unlock(); }}
}

六、生产实践与踩坑清单

  1. 务必用 Lua 校验 token 再解锁:防止误删他人锁。
  2. TTL 要合理:不能太短(业务未完成锁已过期),也不能太长(死锁恢复慢)。一般结合看门狗更稳。
  3. 等待 + 退避:避免 CPU 自旋和惊群;可以配合“排队提示”。
  4. 可重入只是“线程内”语义:跨线程/跨进程不可重入,需要更复杂的标识管理;尽量避免跨线程使用同一锁。
  5. 幂等设计:即使拿到锁也可能重复执行(重试、网络抖动);写操作要有幂等键。
  6. 多节点/主从复制延迟:强一致要求下尽量连接主节点;或降低读从库。
  7. 集群模式 key tag:使用 {} 包裹哈希标签,确保同一键路由到同槽位(适用于 Redisson 等场景)。
  8. 监控指标:加锁成功率、平均等待、续期失败次数、异常堆栈等,配合告警。
  9. 故障演练:kill -9 模拟进程崩溃,验证锁自动过期与业务补偿是否生效。

七、完整配置(参考)

<!-- pom.xml 关键依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
# application.yml
spring:redis:host: localhostport: 6379# password: yourpasslettuce:pool:max-active: 8max-idle: 8min-idle: 0
// Redis 序列化(可选,锁用不到复杂序列化,这里保证 key=String 即可)
@Configuration
public class RedisConfig {@Beanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory f) {return new StringRedisTemplate(f);}
}

八、如何验证

  • 并发压测两个请求同时调用 @RedisLock 方法,观察只有一个进入临界区;另一个要么等待成功、要么超时失败。
  • 人为延长业务耗时(Thread.sleep),观察续期是否发生:在 Redis 中 PTTL lock:... 始终大于 0。
  • 杀掉进程:确认锁会在 TTL 到期后自动释放。

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

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

相关文章

数据结构 -- 链表--双向链表的特点、操作函数

双向链表的操作函数DouLink.c#include "DouLink.h" #include <stdio.h> #include <stdlib.h> #include <string.h>/*** brief 创建一个空的双向链表* * 动态分配双向链表管理结构的内存&#xff0c;并初始化头指针和节点计数* * return 成功返回指…

Wireshark获取数据传输的码元速率

一、Wireshark的物理层参数 Wireshark主界面可以看到数据发送时刻和长度&#xff1a; 这个时刻是Wireshark完整获取数据包的时刻&#xff0c;实际上就是结束时刻。 需要知道的是&#xff1a; Wireshark工作在数据链路层及以上&#xff0c;它能解码 以太网帧 / IP 包 / TCP…

11.1.3 完善注册登录,实现文件上传和展示

1、完善注册/登录 1. 涉及的数据库表单&#xff1a;user_info 2. 引用MySQL线程池&#xff0c;Redis线程池 3. 完善注册功能 4. 完善登录功能 2.1 涉及的数据库表单&#xff1a;user_info 重新创建数据库 #创建数据库 DROP DATABASE IF EXISTS 0voice_tuchuang;CREATE D…

【Linux文件系统】目录结构

有没有刚进入Linux世界时&#xff0c;对着黑乎乎的终端&#xff0c;输入一个 ls / 后&#xff0c;看着蹦出来的一堆名字 like bin, etc, usr&#xff0c;感觉一头雾水&#xff0c;像是在看天书&#xff1f; 别担心&#xff0c;你不是一个人。Linux的文件系统就像一个超级有条理…

螺旋槽曲面方程的数学建模与偏导数求解

螺旋槽曲面的数学描述 在钻头设计和机械加工领域,螺旋槽的几何建模至关重要。螺旋槽通常由径向截形绕轴做螺旋运动形成,其数学模型可通过参数方程和隐函数方程两种方式描述。 设螺旋槽的径向截形方程为: y=f(z)y = f(z)y=f(z) x=xcx = x_cx=xc​ 其中 xcx_cxc​ 为常数,…

线性回归:机器学习中的基石

在机器学习的众多算法中&#xff0c;线性回归无疑是最基础也是最常被提及的一种。它不仅在统计学中占有重要地位&#xff0c;而且在预测分析和数据建模中也发挥着关键作用。本文将深入探讨线性回归的基本概念、评估指标以及在实际问题中的应用&#xff0c;并通过一个模拟的气象…

编程刷题-资料分发1 图论/DFS

P2097 资料分发 1 题目描述 有一些电脑&#xff0c;一部分电脑有双向数据线连接。 如果一个电脑得到数据&#xff0c;它可以传送到的电脑都可以得到数据。 现在&#xff0c;你有这个数据&#xff0c;问你至少将其输入几台电脑&#xff0c;才能使所有电脑得到数据。 输入格式 第…

RabbitMQ:延时消息(死信交换机、延迟消息插件)

目录一、死信交换机【不推荐】二、延迟消息插件【推荐】2.1 安装插件【Linux】2.2 安装插件【Windows】2.3 如何使用延时消息&#xff1a;生产者发送消息时指定一个时间&#xff0c;消费者不会立刻收到消息&#xff0c;而是在指定时间之后才收到消息。 延时任务&#xff1a;设置…

动学学深度学习05-深度学习计算

动学学深度学习pytorch 参考地址&#xff1a;https://zh.d2l.ai/ 文章目录动学学深度学习pytorch1-第05章-深度学习计算1. 层&#xff08;Layer&#xff09;与块&#xff08;Block&#xff09;1.1 什么是深度学习中的“层”&#xff1f;1.2 什么是“块”&#xff08;Block&…

智慧工厂烟雾检测:全场景覆盖与精准防控

智慧工厂烟雾检测&#xff1a;构建工业安全的智能防线&#xff08;所有图片均为真实项目案例&#xff09;在工业4.0时代&#xff0c;智慧工厂通过物联网、人工智能与大数据技术的深度融合&#xff0c;实现了生产流程的数字化与智能化。然而&#xff0c;工厂环境中的火灾隐患始终…

@JsonIgnoreProperties注解详解

JsonIgnoreProperties是 Jackson 库中的一个重要注解&#xff0c;用于在 JSON 序列化&#xff08;对象转 JSON&#xff09;和反序列化&#xff08;JSON 转对象&#xff09;过程中​​控制属性的可见性​​。它提供了更高级别的属性忽略能力&#xff0c;特别适合处理复杂场景。一…

红酒数据集预处理实战:缺失值处理的 5 种打开方式,从入门到进阶一步到位

在数据分析与建模流程中&#xff0c;缺失值处理是数据预处理阶段的关键步骤&#xff0c;直接影响后续模型的准确性与稳定性。本文以红酒数据集为研究对象&#xff0c;详细介绍如何通过基础统计方法&#xff08;均值、中位数、众数&#xff09;、完整案例分析&#xff08;CCA&am…

Node.js 开发 JavaScript SDK 包的完整指南(AI)

一、核心概念SDK 包定义 专为特定服务/平台封装的工具库&#xff0c;提供标准化 API 调用、错误处理、类型声明等功能。示例&#xff1a;支付宝 SDK、AWS SDK、微信小程序 SDK。技术栈选择 语言&#xff1a;JavaScript/TypeScript&#xff08;推荐 TS&#xff0c;便于类型提示&…

Redis实战-基于Session实现分布式登录

1.流程分析1.1发送短信验证码提交手机号的时候要进行校验手机号&#xff0c;校验成功才会去生成验证码&#xff0c;将验证码保存到session&#xff0c;发生他把这部分那。1.2短信验证码登录/注册如果提交手机号和验证码之后&#xff0c;校验一致才进行根据手机号查询用户&#…

疯狂星期四文案网第47天运营日记

网站运营第47天&#xff0c;点击观站&#xff1a; 疯狂星期四 crazy-thursday.com 全网最全的疯狂星期四文案网站 运营报告 今日访问量 今日搜索引擎收录情况 必应现在是边收录边k页面 百度快倒闭 网站优化点 完善工作流&#xff0c;全面实现文案自动化采集&#xff0c;se…

Vue生命周期以及自定义钩子和路由

Vue生命周期常用的onMounted挂载后执行和onUnmounted卸载前以及onupdated更新后实际上用react对比就是useEffect&#xff0c;而且挂载顺序也是子组件先于父组件然后往外的栈结构&#xff0c;先进后出。1.Vue的生命周期<template><h2>当前求和为{{ sum }}</h2>…

探索Thompson Shell:Unix初代Shell的智慧

引言 在计算机科学的漫漫长河中&#xff0c;Thompson Shell 无疑占据着举足轻重的开创性地位&#xff0c;它是 Unix 系统的第一个 shell&#xff0c;诞生于 1971 年&#xff0c;由计算机领域的传奇人物 Ken Thompson 开发。在那个计算机技术刚刚起步、硬件资源极度匮乏的年代&a…

MySQL B+ 树索引详解:从原理到实战优化

引言在现代数据库应用中&#xff0c;查询效率是影响系统性能的关键因素之一。而索引&#xff0c;尤其是 B 树索引&#xff0c;是 MySQL 中最常用、最重要的性能优化手段。正确使用索引可以将查询时间从毫秒级降低到微秒级&#xff0c;极大地提升应用响应速度。1. B 树索引的重要…

计算机内存中的整型存储奥秘、大小端字节序及其判断方法

目录 一、回顾与引入&#xff1a;整数在内存中的存储方式 为什么要采用补码存储&#xff1f; 二、大小端字节序及其判断方法 1、什么是大小端&#xff1f; 2、为什么存在大小端&#xff1f; 3、练习 练习1&#xff1a;简述大小端概念并设计判断程序&#xff08;百度面试…

Redis 最常用的 5 种数据类型

Redis 支持多种灵活的数据类型&#xff0c;每种类型针对特定场景优化。以下是 **Redis 最常用的 5 种数据类型**及其核心特点和应用场景&#xff1a;1. 字符串&#xff08;String&#xff09;描述&#xff1a;最基本的数据类型&#xff0c;可存储文本、数字&#xff08;整数/浮…