分布式锁实现说明
本文档基于 需求:Redis 分布式锁(Token 语义) 与当前工程代码,说明本项目的分布式锁实现了什么、如何组成、如何配置与使用。
1. 需求概述
1.1 实现了什么
- 全局互斥:项目内通过固定 Redis Key
lock:manage:global:mutex(RedisDistLockService.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 会DELkey,自动释放并停止续期任务。
- 初始租约
- 自动释放的多种路径:业务手动
unLock;达到最长持有后脚本自动删键;续期未执行或失败导致 TTL 自然过期后锁消失。 - 配置可热更新:
distlock.*建议放在 Nacosmanage.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.xml 的 dependencyManagement 中已导入该 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.yml(refresh: 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成功后startRenewTask:scheduleWithFixedDelay,间隔至少 1s,即max(1000, renewIntervalMs)。unLock等路径中stopRenewTask:remove+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 基本步骤
- 注入
DistLockService(实现类为RedisDistLockService)。 LockToken token = distLockService.tryLock();token == null时未拿到锁,应直接返回/降级,不要阻塞等待。- 在任意线程中完成互斥逻辑后
distLockService.unLock(token);建议在try/finally中调用。 - 修改 Nacos 中
distlock.*后如需立刻重读内存配置,可调用distLockService.refreshConfig()。
unLock 在锁不存在、非 owner、Redis 异常等情况下返回 false,不抛业务异常。
3.2 与演示代码的对应关系
- 主线程
tryLock、工作线程unLock的完整写法见 §2.6(DistLockDemoExecutorConfig、DistLockDemoController)。 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/合并后的 Environment;refreshConfig() 手动刷新内存配置 |
| 使用要点 | 先 tryLock,失败不等待;在 finally 中 unLock;异步场景可跨线程传 LockToken |
