当前位置: 首页 > news >正文

redis分布式锁的实现

分布式锁实现说明

本文档基于 需求:Redis 分布式锁(Token 语义) 与当前工程代码,说明本项目的分布式锁实现了什么、如何组成、如何配置与使用


1. 需求概述

1.1 实现了什么

  • 全局互斥:项目内通过固定 Redis Key lock:manage:global:mutexRedisDistLockService.GLOBAL_LOCK_KEY)实现一把锁,让需要互斥的多个业务方法(约 3~10 个)在集群间互斥执行。
  • Token 语义、不绑定线程:加锁成功返回 LockToken,其中 taskId 为持有者标识;续期与释放只校验 taskId,因此可以在非获取锁的线程中释放(例如 Controller 中 tryLock,在线程池任务中 unLock),满足异步场景。
  • 显式 API不使用注解,业务显式调用 tryLock() / unLock(LockToken)tryLock 立即返回、不等待不重试(拿不到锁返回 null)。
  • 时间语义
    • 初始租约 initialLeaseMs:加锁后 Key 的 TTL,续期时也会重置为该值。
    • 续期间隔 renewIntervalMs:内部守护任务按此周期执行续期 Lua。
    • 最长持有 maxHoldMs:从 acquiredAt(Redis 时间)起算,达到后续期 Lua 会 DEL key,自动释放并停止续期任务。
  • 自动释放的多种路径:业务手动 unLock;达到最长持有后脚本自动删键;续期未执行或失败导致 TTL 自然过期后锁消失。
  • 配置可热更新distlock.* 建议放在 Nacos manage.yml 等配置中;refreshConfig()Environment 再读一次并更新内存中的 LockConfig;读失败时打日志并保留旧配置。

1.2 为什么要这样实现

  • Redis + Hash + TTL:用 Hash 存 owner(taskId)与 acquiredAt(毫秒),用 Key 的 PEXPIRE 表示当前租约;与「仅 SETNX + 简单过期」相比,能表达最长持有时长(与 acquiredAt 比较)并在 Lua 中原子完成续期/判断/删键。
  • Lua 脚本:加锁、续期、释放均在服务端原子执行,避免 GET/SET 分离导致的竞态。
  • 续期守护:单线程 ScheduledExecutorService + 每 taskId 一个 ScheduledFuture,与需求中的全局调度模型一致,释放时取消续期任务,避免已释放的锁仍被续期。

2. 实现组成(按落地顺序)

约定:下文中 Java 代码块不含 package ... 行,其余与仓库源文件一致;XML / YAML / Lua 为原文

2.1 依赖(Maven) — pom.xml

实现本分布式锁新引入的依赖如下:前两项用于 Nacos 配置注入与 bootstrap 提前加载,第三项为 Redis(StringRedisTemplate、Lua 脚本执行)。Nacos 相关 artifact 的版本由 spring-cloud-alibaba-dependencies BOM 管理,需在 pom.xmldependencyManagement 中已导入该 BOM(与工程现有配置一致,此处不重复贴出完整 pom)。

        <!-- Nacos 配置中心(distlock 等从 Nacos 经 Environment 读取) --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency><!-- Redis:分布式锁(Lua + TTL + 续期) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

2.2 配置

(1)src/main/resources/application.yml — Redis

spring:profiles:active: devredis:# Redis 连接信息(建议用环境变量/VM 参数注入,避免写死在仓库里)host: ${REDIS_HOST:127.0.0.1}port: ${REDIS_PORT:6379}password: ${REDIS_PASSWORD:}database: ${REDIS_DATABASE:0}timeout: ${REDIS_TIMEOUT_MS:3000ms}# Lettuce 连接池(Spring Boot 2.7 默认客户端)lettuce:pool:max-active: ${REDIS_POOL_MAX_ACTIVE:16}max-idle: ${REDIS_POOL_MAX_IDLE:8}min-idle: ${REDIS_POOL_MIN_IDLE:0}max-wait: ${REDIS_POOL_MAX_WAIT_MS:3000ms}

(2)src/main/resources/bootstrap.yml — Nacos

extension-configs 中加载 manage.ymlrefresh: true),其中的 distlock 会合并进 Environment

spring:cloud:nacos:config:# Nacos 地址:本地/服务器可通过环境变量覆盖server-addr: ${NACOS_HOST}# Nacos 命名空间:必须填【namespaceId】(通常是 UUID),不是 namespace 名称# 如果你创建命名空间时把 namespaceId 直接设为 dev,这里填 dev 才会生效namespace: devgroup: DEFAULT_GROUPfile-extension: ymlprefix: manageusername: nacospassword: ${NACOS_PASSWORD}# 先加载公共配置(通用),再加载环境配置(manage-${profile}.yml)覆盖差异项extension-configs:- data-id: manage.ymlgroup: DEFAULT_GROUPrefresh: truerefresh-enabled: true

(3)Nacos:manage.yml(或同组覆盖)— distlock

未配置或解析失败时,代码使用默认 15000 / 10000 / 30 * 60 * 1000(30 分钟)。

distlock:initialLeaseMs: 15000renewIntervalMs: 10000maxHoldMs: 1800000

(4)可选
spring.application.name 会作为 LockToken.instanceId 注入。


2.3 数据结构与接口(Java 源码)

src/main/java/com/manage/lock/LockConfig.java

import lombok.Builder;
import lombok.Value;/*** 分布式锁配置(来自 Nacos/配置中心,缺失则使用默认值)。*/
@Value
@Builder
public class LockConfig {/*** 初始租约(毫秒),获取锁成功后设置 TTL,续期时也重置为该 TTL。*/long initialLeaseMs;/*** 续期间隔(毫秒),守护任务按该间隔触发 renew。*/long renewIntervalMs;/*** 最长持有(毫秒),达到后 Lua 脚本会自动 DEL key 并返回特定 code。*/long maxHoldMs;
}

src/main/java/com/manage/lock/LockToken.java

import lombok.Builder;
import lombok.Value;/*** 分布式锁 token(owner 语义)。* <p>* 业务在 tryLock 成功后持有该 token,在 unLock 时原样传回;* 续期与释放都只认 token 内的 taskId,不绑定线程。* </p>*/
@Value
@Builder
public class LockToken {/*** 锁 key(建议固定常量,便于全局互斥与日志定位)。*/String lockKey;/*** 唯一持有者标识(UUID/随机串)。*/String taskId;/*** 获取成功的本地时间戳(毫秒),用于日志/排障。*/long acquiredAtMs;/*** 最长持有截止时间(毫秒):acquiredAtMs + maxHoldMs,仅用于提示/日志。*/long maxHoldDeadlineMs;/*** 节点标识(可选):用于日志定位是哪个实例拿到锁。*/String instanceId;
}

src/main/java/com/manage/lock/DistLockService.java

/*** 分布式锁服务(全局互斥:不等待、不重试)。*/
public interface DistLockService {/*** 尝试加锁(立即返回,不等待)。** @return 加锁成功返回 token;失败(已被占用等)返回 null*/LockToken tryLock();/*** 释放锁(幂等:锁不存在/非 owner 时返回 false,不抛异常)。** @param token tryLock() 返回的 token* @return true 表示释放成功;false 表示锁不存在/非 owner/异常等*/boolean unLock(LockToken token);/*** 刷新配置(从 Nacos/Environment 重新读取,并替换内存中的最后一次有效配置)。* <p>* 失败策略:记录日志,继续使用旧配置。* </p>*/void refreshConfig();
}

src/main/java/com/manage/lock/RedisDistLockService.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
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.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;/*** 基于 Redis Lua 的分布式锁实现:* <p>* - token(owner) 语义:通过 taskId 校验 owner,不绑定线程* - 续期守护:定时 renew,直到锁过期/owner 变化/到达最长持有* - 配置来自 Nacos:从 Environment 读取 distlock.*,refreshConfig() 可手动刷新* </p>*/
@Slf4j
@Service
public class RedisDistLockService implements DistLockService, DisposableBean {/*** 全局互斥的锁 key(需要互斥的 3~10 个方法共用该 key)。*/public static final String GLOBAL_LOCK_KEY = "lock:manage:global:mutex";/*** Lua 返回码:成功(ACQUIRED / RENEWED / RELEASED)。*/private static final long CODE_OK = 0L;/*** Lua 返回码:锁不存在(通常表示已过期释放或已被删除)。*/private static final long CODE_NOT_FOUND = 1L;/*** Lua 返回码:非 owner(taskId 不匹配,表示锁被其他持有者占用)。*/private static final long CODE_NOT_OWNER = 2L;/*** Lua 返回码:达到最长持有并已自动释放(脚本内部已执行 DEL)。*/private static final long CODE_MAX_HOLD_RELEASED = 3L;/*** 默认初始租约(毫秒):设置锁的 TTL,续期时也重置为该值。*/private static final long DEFAULT_INITIAL_LEASE_MS = 15_000L;/*** 默认续期间隔(毫秒):续期守护任务的执行周期。*/private static final long DEFAULT_RENEW_INTERVAL_MS = 10_000L;/*** 默认最长持有(毫秒):到达后不再续期并自动释放。*/private static final long DEFAULT_MAX_HOLD_MS = 30 * 60_000L;/*** 执行 Lua 脚本与 Redis 命令的客户端。*/private final StringRedisTemplate stringRedisTemplate;/*** 配置环境,用于读取 distlock.* 及刷新后的值。*/private final Environment environment;/*** 内存中的锁参数(租约、续期间隔、最长持有),支持无锁读取与原子替换。*/private final AtomicReference<LockConfig> lockConfigRef = new AtomicReference<>();/*** 续期守护线程池(单线程即可,按 taskId 调度多个定时任务)。*/private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, new CustomizableThreadFactory("distlock-renew-"));/*** taskId → 续期 {@link ScheduledFuture},释放锁时取消对应任务。*/private final Map<String, ScheduledFuture<?>> renewTaskMap = new ConcurrentHashMap<>();/*** tryLock 对应的 Lua(加锁)。*/private RedisScript<Long> tryLockScript;/*** 续期对应的 Lua。*/private RedisScript<Long> renewScript;/*** unLock 对应的 Lua(释放)。*/private RedisScript<Long> unLockScript;/*** 仅用于日志定位当前实例(可通过配置覆盖)。*/@Value("${spring.application.name:manage}")private String instanceId;public RedisDistLockService(StringRedisTemplate stringRedisTemplate, Environment environment) {this.stringRedisTemplate = stringRedisTemplate;this.environment = environment;}/*** 初始化 Lua 脚本与初始配置(启动时加载一次)。*/@PostConstructpublic void init() {this.tryLockScript = loadLongScript("lua/distlock_try_acquire.lua");this.renewScript = loadLongScript("lua/distlock_renew.lua");this.unLockScript = loadLongScript("lua/distlock_release.lua");// 启动时先加载一份配置,后续 refreshConfig() 失败时也能兜底运行this.lockConfigRef.set(readConfigFromEnvironmentSafely(null));log.info("distlock init ok. lockKey={}, config={}", GLOBAL_LOCK_KEY, lockConfigRef.get());}/*** 尝试加锁:成功则返回 token 并启动续期守护;失败(锁已被占用等)返回 null。*/@Overridepublic LockToken tryLock() {LockConfig cfg = lockConfigRef.get();String taskId = UUID.randomUUID().toString().replace("-", "");Long code = execute(tryLockScript, GLOBAL_LOCK_KEY, taskId, String.valueOf(cfg.getInitialLeaseMs()));if (code == null) {log.warn("分布式锁 获取错误. lockKey={}, taskId={}", GLOBAL_LOCK_KEY, taskId);return null;}if (code == CODE_OK) {long now = System.currentTimeMillis();LockToken token = LockToken.builder().lockKey(GLOBAL_LOCK_KEY).taskId(taskId).acquiredAtMs(now).maxHoldDeadlineMs(now + cfg.getMaxHoldMs()).instanceId(instanceId).build();startRenewTask(token);log.info("已获得分布式锁. lockKey={}, taskId={}, leaseMs={}, maxHoldMs={}",GLOBAL_LOCK_KEY, taskId, cfg.getInitialLeaseMs(), cfg.getMaxHoldMs());return token;}// BUSYlog.debug("分布式锁获取繁忙. lockKey={}", GLOBAL_LOCK_KEY);return null;}/*** 释放锁:先取消续期任务,再执行释放脚本;是否释放成功见返回值(幂等)。*/@Overridepublic boolean unLock(LockToken token) {if (token == null) {return false;}// 先取消续期任务,避免释放后仍在续期(并发场景下允许一次“多余续期”)stopRenewTask(token.getTaskId());Long code = execute(unLockScript, token.getLockKey(), token.getTaskId());if (code == null) {log.error("锁释放错误. lockKey={}, taskId={}", token.getLockKey(), token.getTaskId());return false;}if (code == CODE_OK) {log.info("锁正确释放. lockKey={}, taskId={}", token.getLockKey(), token.getTaskId());return true;}// NOT_FOUND / NOT_OWNER:按要求静默失败,不抛业务异常if (code == CODE_NOT_FOUND) {log.info("释放锁 --> 锁不存在. lockKey={}, taskId={}", token.getLockKey(), token.getTaskId());} else if (code == CODE_NOT_OWNER) {log.warn("释放锁 --> taskId 不匹配,锁被其他持有者占用. lockKey={}, taskId={}", token.getLockKey(), token.getTaskId());} else {log.warn("释放锁 --> 其他错误. lockKey={}, taskId={}, code={}",token.getLockKey(), token.getTaskId(), code);}return false;}/*** 手动刷新配置:从 Nacos/Environment 重新读取 distlock.* 并覆盖内存配置。* <p>* 失败策略:打印日志并保留旧值。* </p>*/@Overridepublic void refreshConfig() {LockConfig oldCfg = lockConfigRef.get();LockConfig newCfg = readConfigFromEnvironmentSafely(oldCfg);lockConfigRef.set(newCfg);log.info("锁的相关配置刷新成功. old={}, new={}", oldCfg, newCfg);}/*** 停止组件:取消所有续期任务并关闭线程池。*/@Overridepublic void destroy() {for (Map.Entry<String, ScheduledFuture<?>> e : renewTaskMap.entrySet()) {ScheduledFuture<?> f = e.getValue();if (f != null) {f.cancel(true);}}renewTaskMap.clear();scheduler.shutdownNow();}/*** 启动续期任务:按 renewIntervalMs 周期执行 renew Lua。*/private void startRenewTask(LockToken token) {// 防重复:同一 taskId 理论上不会重复,但这里做一层保护stopRenewTask(token.getTaskId());long intervalMs = Math.max(1000L, lockConfigRef.get().getRenewIntervalMs());ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(() -> doRenew(token),intervalMs,intervalMs,TimeUnit.MILLISECONDS);renewTaskMap.put(token.getTaskId(), future);}/*** 取消续期任务(幂等)。*/private void stopRenewTask(String taskId) {ScheduledFuture<?> future = renewTaskMap.remove(taskId);if (future != null) {// false:勿中断正阻塞在 EVAL 的续期线程,否则 Lettuce 会报 RedisCommandInterruptedExceptionfuture.cancel(false);}}/*** 执行一次续期:根据返回码决定是否停止续期。*/private void doRenew(LockToken token) {LockConfig cfg = lockConfigRef.get();Long code = execute(renewScript,token.getLockKey(),token.getTaskId(),String.valueOf(cfg.getInitialLeaseMs()),String.valueOf(cfg.getMaxHoldMs()));if (code == null) {// Redis 异常:按文档建议避免刷屏,这里仅 ERROR 记录一次,后续继续尝试log.error("锁续期异常. lockKey={}, taskId={}", token.getLockKey(), token.getTaskId());return;}if (code == CODE_OK) {// 续期成功:debug 级别,避免刷日志log.debug("锁续期成功. lockKey={}, taskId={}", token.getLockKey(), token.getTaskId());return;}// 下面这些情况都应停止续期if (code == CODE_NOT_FOUND) {log.warn("续期锁异常 --> 锁不存在: not found. lockKey={}, taskId={}", token.getLockKey(), token.getTaskId());} else if (code == CODE_NOT_OWNER) {log.warn("续期锁异常 --> 非 owner. lockKey={}, taskId={}", token.getLockKey(), token.getTaskId());} else if (code == CODE_MAX_HOLD_RELEASED) {log.warn("续期锁异常 --> 达到最长持有并已自动释放. lockKey={}, taskId={}, deadlineMs={}",token.getLockKey(), token.getTaskId(), token.getMaxHoldDeadlineMs());} else {log.warn("续期锁异常 --> 其他异常: unknown code. lockKey={}, taskId={}, code={}",token.getLockKey(), token.getTaskId(), code);}stopRenewTask(token.getTaskId());}/*** 从 Environment 读取 distlock 配置(解析失败时回退到旧值或默认值)。*/private LockConfig readConfigFromEnvironmentSafely(LockConfig oldCfg) {try {long initialLeaseMs = getLong("distlock.initialLeaseMs", DEFAULT_INITIAL_LEASE_MS);long renewIntervalMs = getLong("distlock.renewIntervalMs", DEFAULT_RENEW_INTERVAL_MS);long maxHoldMs = getLong("distlock.maxHoldMs", DEFAULT_MAX_HOLD_MS);// 续期间隔不能大于租约,否则容易出现“那一次续期来不及导致过期释放”// 按需求不强拦截,因此这里只做 WARN,不做强制纠正if (renewIntervalMs >= initialLeaseMs) {log.warn("distlock config maybe risky: renewIntervalMs >= initialLeaseMs. renewIntervalMs={}, initialLeaseMs={}",renewIntervalMs, initialLeaseMs);}return LockConfig.builder().initialLeaseMs(initialLeaseMs).renewIntervalMs(renewIntervalMs).maxHoldMs(maxHoldMs).build();} catch (Exception ex) {log.error("distlock read config failed, keep old. old={}", oldCfg, ex);return oldCfg != null ? oldCfg : LockConfig.builder().initialLeaseMs(DEFAULT_INITIAL_LEASE_MS).renewIntervalMs(DEFAULT_RENEW_INTERVAL_MS).maxHoldMs(DEFAULT_MAX_HOLD_MS).build();}}/*** 从 Environment 读取 long 值(缺失或不可解析则用默认值)。*/@SuppressWarnings("null")private long getLong(String key, long defaultVal) {String val = environment.getProperty(key);if (val == null || val.trim().isEmpty()) {return defaultVal;}try {return Long.parseLong(val.trim());} catch (NumberFormatException ex) {log.warn("distlock config parse failed, use default. key={}, val={}", key, val);return defaultVal;}}/*** 执行 Lua 脚本(返回 Long),执行异常返回 null。*/@SuppressWarnings("null")private Long execute(RedisScript<Long> script, String key, String... args) {try {// StringRedisTemplate 的 execute 在 IDE 的空安全检查下会有泛型告警,这里按实际签名安全调用即可return stringRedisTemplate.execute(script, Collections.singletonList(key), (Object[]) args);} catch (Exception ex) {log.error("distlock redis execute failed. key={}", key, ex);return null;}}/*** 加载 classpath 下 Lua 脚本,并包装为返回 Long 的 RedisScript。*/@SuppressWarnings("null")private RedisScript<Long> loadLongScript(String classpath) {try {ClassPathResource resource = new ClassPathResource(classpath);byte[] bytes = StreamUtils.copyToByteArray(resource.getInputStream());String scriptText = new String(bytes, StandardCharsets.UTF_8);DefaultRedisScript<Long> script = new DefaultRedisScript<>();script.setScriptText(scriptText);script.setResultType(Long.class);return script;} catch (Exception ex) {throw new IllegalStateException("load lua script failed: " + classpath, ex);}}
}

2.4 Lua 脚本(原文)

src/main/resources/lua/distlock_try_acquire.lua

-- 分布式锁:尝试获取锁(不等待)
-- KEYS[1] = lockKey
-- ARGV[1] = taskId
-- ARGV[2] = initialLeaseMs
--
-- 返回码:
-- 0 = ACQUIRED
-- 1 = BUSYlocal key = KEYS[1]
local taskId = ARGV[1]
local initialLeaseMs = tonumber(ARGV[2])if redis.call('EXISTS', key) == 0 thenlocal t = redis.call('TIME')local nowMs = (t[1] * 1000) + math.floor(t[2] / 1000)redis.call('HSET', key, 'owner', taskId, 'acquiredAt', tostring(nowMs))redis.call('PEXPIRE', key, initialLeaseMs)return 0
endreturn 1

src/main/resources/lua/distlock_renew.lua

-- 分布式锁:续期(仅 owner 可续期;支持最长持有,到期自动释放)
-- KEYS[1] = lockKey
-- ARGV[1] = taskId
-- ARGV[2] = initialLeaseMs
-- ARGV[3] = maxHoldMs
--
-- 返回码:
-- 0 = RENEWED
-- 1 = NOT_FOUND
-- 2 = NOT_OWNER
-- 3 = MAX_HOLD_REACHED_RELEASED(已 DEL)local key = KEYS[1]
local taskId = ARGV[1]
local initialLeaseMs = tonumber(ARGV[2])
local maxHoldMs = tonumber(ARGV[3])if redis.call('EXISTS', key) == 0 thenreturn 1
endlocal owner = redis.call('HGET', key, 'owner')
if (not owner) or owner ~= taskId thenreturn 2
endlocal acquiredAtStr = redis.call('HGET', key, 'acquiredAt')
local acquiredAt = tonumber(acquiredAtStr)
if (not acquiredAt) then-- acquiredAt 缺失时,为避免无限持有,直接按“不存在”处理,让续期停止return 1
endlocal t = redis.call('TIME')
local nowMs = (t[1] * 1000) + math.floor(t[2] / 1000)
local heldMs = nowMs - acquiredAtif heldMs >= maxHoldMs thenredis.call('DEL', key)return 3
endredis.call('PEXPIRE', key, initialLeaseMs)
return 0

src/main/resources/lua/distlock_release.lua

-- 分布式锁:释放(仅 owner 可释放)
-- KEYS[1] = lockKey
-- ARGV[1] = taskId
--
-- 返回码:
-- 0 = RELEASED
-- 1 = NOT_FOUND
-- 2 = NOT_OWNERlocal key = KEYS[1]
local taskId = ARGV[1]if redis.call('EXISTS', key) == 0 thenreturn 1
endlocal owner = redis.call('HGET', key, 'owner')
if (not owner) or owner ~= taskId thenreturn 2
endredis.call('DEL', key)
return 0

2.5 续期与线程模型(说明)

  • 单线程池:newScheduledThreadFactory("distlock-renew-")
  • tryLock 成功后 startRenewTaskscheduleWithFixedDelay,间隔至少 1s,即 max(1000, renewIntervalMs)
  • unLock 等路径中 stopRenewTaskremove + cancel(false),避免中断正在 EVAL 的续期线程(见 RedisDistLockService 注释)。

2.6 演示代码(非锁核心,仅用法示例)

src/main/java/com/manage/config/DistLockDemoExecutorConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;/*** 演示用线程池:在 Controller 中通过 {@link java.util.concurrent.Executor#execute} 提交持锁任务。*/
@Configuration
public class DistLockDemoExecutorConfig {@Beanpublic ThreadPoolTaskExecutor distLockDemoExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(2);executor.setMaxPoolSize(8);executor.setQueueCapacity(200);executor.setThreadNamePrefix("distlock-demo-");executor.initialize();return executor;}
}

src/main/java/com/manage/controller/DistLockDemoController.java

import com.xxx.manage.lock.DistLockService;
import com.xxx.manage.lock.LockToken;
import com.xxx.manage.model.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;/*** 演示:tryLock 获取全局分布式锁后在线程池任务中执行业务,在 finally 中 unLock 释放锁。*/
@Slf4j
@RestController
@RequestMapping("/demo/dist-lock")
@RequiredArgsConstructor
@Tag(name = "分布式锁示例", description = "tryLock 后异步执行业务,结束时 unLock")
public class DistLockDemoController {private final DistLockService distLockService;@Qualifier("distLockDemoExecutor")private final Executor distLockDemoExecutor;@PostMapping("/async-sleep")@Operation(summary = "持锁异步休眠示例", description = "无入参;tryLock 成功则启动异步任务(日志 + 睡 10s),结束后再 unLock 并停止续期")public Result<String> asyncSleep() {log.info("开始 tryLock...");LockToken token = distLockService.tryLock();if (token == null) {log.info("未获取到锁,可能正被其他请求占用");return Result.fail(409, "未获取到锁,可能正被其他请求占用");}log.info("tryLock 成功,taskId={}", token.getTaskId());distLockDemoExecutor.execute(() -> {try {log.info("进入异步方法内部, taskId={}, 开始沉睡...", token.getTaskId());for (int i = 0; i < 100; i++) {log.info("开始沉睡第 {} 次", i + 1);TimeUnit.SECONDS.sleep(1);}log.info("沉睡完成..., taskId={}", token.getTaskId());} catch (InterruptedException e) {Thread.currentThread().interrupt();log.warn("进入catch块内部, taskId={}", token.getTaskId());} finally {log.info("业务方法完成,开始 unLock...");distLockService.unLock(token);log.info("unLock 成功, taskId={}", token.getTaskId());}});return Result.success("已接受请求,后台任务已启动,持锁约 10s 后自动释放");}
}

生产业务可参照 §2.6 的跨线程持锁/释锁方式,自行管理线程与异常,不必保留该 Controller。


3. 如何使用

3.1 基本步骤

  1. 注入 DistLockService(实现类为 RedisDistLockService)。
  2. LockToken token = distLockService.tryLock()token == null 时未拿到锁,应直接返回/降级,不要阻塞等待
  3. 任意线程中完成互斥逻辑后 distLockService.unLock(token);建议在 try/finally 中调用。
  4. 修改 Nacos 中 distlock.* 后如需立刻重读内存配置,可调用 distLockService.refreshConfig()

unLock 在锁不存在、非 owner、Redis 异常等情况下返回 false不抛业务异常

3.2 与演示代码的对应关系

  • 主线程 tryLock、工作线程 unLock 的完整写法见 §2.6DistLockDemoExecutorConfigDistLockDemoController)。
  • LockToken 经闭包传入异步任务,释锁线程与加锁线程不同unLock 应对同一 token 只走一条正常释放路径(重复释放会返回 false)。
  • 若业务耗时可能超过 maxHoldMs,续期侧会删键,后续 unLock 可能为 false,需有超时/重试等策略。

HTTP 调用示例(仅演示接口):

POST /demo/dist-lock/async-sleep

4. 总结

方面 说明
互斥范围 固定 Key lock:manage:global:mutex,多实例共用 Redis 时全局互斥
持锁证明 LockToken.taskId 与 Redis Hash 中 owner 一致;与线程无关
租约与续期 initialLeaseMs 为 TTL;后台按 renewIntervalMs 续期;maxHoldMs 到则脚本删键
配置来源 distlock.* 来自 Nacos/合并后的 EnvironmentrefreshConfig() 手动刷新内存配置
使用要点 tryLock,失败不等待;在 finallyunLock;异步场景可跨线程传 LockToken
http://www.jsqmd.com/news/705700/

相关文章:

  • 如何用PyAEDT实现电磁仿真自动化?告别重复点击的终极指南
  • Python异步编程中的上下文管理:Acontext库原理与实践
  • 轻松搞定文件压缩:7-Zip新手完全入门指南
  • 如何快速提取B站视频字幕:终极免费工具使用指南
  • Honcho开源框架:AI智能体会话状态管理与编排实践指南
  • 从零开始掌握NSC_BUILDER:Switch游戏文件管理的瑞士军刀
  • Gemma-4-26B-A4B-it-GGUF入门指南:WebUI中启用streaming响应与禁用流式输出对比体验
  • 贝叶斯定理在机器学习中的应用与实践
  • 四川盛世钢联国际贸易有限公司-全品类建筑钢材供应厂家频道 - 四川盛世钢联营销中心
  • LangGraph 源码逐行解读:Multi-Agent 状态流转与协作的底层架构
  • 如何用WebToEpub一键将网页小说转为EPUB电子书永久保存
  • DeepSeek-R1-Distill-Qwen-1.5B部署成功秘诀:日志查看与问题排查技巧
  • 自动化工作流开发:OCR识别致PDF信息提取、数学计算与Word计算书生成
  • Deepseek V4 Pro 到底好用吗?实测报告来了!
  • 快速构建高质量3D模型的终极指南:Meshroom开源摄影测量工具深度解析
  • 告别虚拟机!在Win11上用WSL2+Miniconda3搭建生信环境,保姆级避坑指南
  • Cat-Catch浏览器扩展终极指南:一站式网页资源嗅探与流媒体捕获解决方案
  • 给出直接 Powershell 降低比特率的命令行
  • WebPages 帮助器
  • LlamaIndex.TS停更启示:从RAG框架设计看LLM应用数据层演进
  • 大语言模型低延迟推理:TTFT优化与GH200架构实践
  • AI Agent Harness Engineering 失败复盘:那些看似聪明却无法落地的常见原因
  • LRCGet:本地音乐库同步歌词自动匹配的终极解决方案
  • 100行代码构建AI智能体:从工具调用原理到本地自动化实战
  • 前端视角:B端传统配置化现状与AI冲击趋势
  • PostgreSQL 视图
  • 基于WebRTC VAD与Web Audio API实现浏览器端智能音频闪避
  • 2026金融行业人员,想转行数据分析有完整路线吗?新手能快速上手吗?
  • Divinity Mod Manager架构解析:神界原罪2模组管理技术实现
  • [特殊字符] EagleEye一文详解:DAMO-YOLO TinyNAS如何通过神经架构搜索压缩模型至3.2MB