可直接落地的 Redis 分布式锁实现:包含最小可用版、生产可用版(带 Lua 原子解锁、续期"看门狗"、自旋等待、可重入)、以及基于注解+AOP 的无侵入用法,最后还给出 Redisson 方案对比与踩坑清单。
一、设计目标与约束
- 获取锁:
SET key value NX PX ttl
(原子、带过期) - 释放锁:Lua 校验 value (token)后再
DEL
,避免误删他人锁 - 等待策略:可设置总体等待时长 + 抖动退避,避免惊群
- 续期(看门狗):长耗时任务自动延长锁过期,避免任务未完成锁先过期
- 可重入:同一线程/请求二次进入同一锁,计数 +1,退出时计数 -1
- 可观测性:日志、指标(命中/失败/续期次数等)
二、最小可用实现(入门示例)
java
// 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] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
Long res = redis.execute(new DefaultRedisScript<>(script, Long.class), List.of(key), token);
return res != null && res > 0;
}
}
适合"单次短任务、不等待"的场景;生产建议使用下文增强版。
三、生产可用锁客户端(可重入 + 等待 + 续期)
1)核心实现
java
// 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] then
return redis.call('del', KEYS[1])
else
return 0
end
""");
renewScript.setResultType(Long.class);
renewScript.setScriptText("""
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('pexpire', KEYS[1], ARGV[2])
else
return 0
end
""");
}
@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();
// 可重入:当前线程已持有同一 key
if (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~150ms
try { 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 自动释放)
java
@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)定义注解
java
// 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 切面
java
// 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)业务使用
java
@Service
public class CheckoutService {
@RedisLock(name = "pay", key = "#orderId", ttlMs = 15000, waitMs = 5000)
public String pay(Long orderId) {
// 幂等校验、扣款、记账、改状态...
return "OK";
}
}
五、和 Redisson 的取舍
- 自己实现(本文方案)
轻量、可控、无第三方依赖;需要你自己维护续期、统计、容错。 - Redisson
功能齐全(公平锁、信号量、读写锁、锁续期看门狗、联锁/红锁等),配置简单,实战成熟。
👉 建议对锁模型复杂、需要多数据结构协作的场景直接上 Redisson。
示例(Redisson):
xml
<!-- pom -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.38.0</version>
</dependency>
java
@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(); }
}
}
六、生产实践与踩坑清单
- 务必用 Lua 校验 token 再解锁:防止误删他人锁。
- TTL 要合理 :不能太短(业务未完成锁已过期),也不能太长(死锁恢复慢)。一般结合看门狗更稳。
- 等待 + 退避:避免 CPU 自旋和惊群;可以配合"排队提示"。
- 可重入只是"线程内"语义:跨线程/跨进程不可重入,需要更复杂的标识管理;尽量避免跨线程使用同一锁。
- 幂等设计:即使拿到锁也可能重复执行(重试、网络抖动);写操作要有幂等键。
- 多节点/主从复制延迟:强一致要求下尽量连接主节点;或降低读从库。
- 集群模式 key tag :使用
{}
包裹哈希标签,确保同一键路由到同槽位(适用于 Redisson 等场景)。 - 监控指标:加锁成功率、平均等待、续期失败次数、异常堆栈等,配合告警。
- 故障演练:kill -9 模拟进程崩溃,验证锁自动过期与业务补偿是否生效。
七、完整配置(参考)
xml
<!-- 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>
yaml
# application.yml
spring:
redis:
host: localhost
port: 6379
# password: yourpass
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
java
// Redis 序列化(可选,锁用不到复杂序列化,这里保证 key=String 即可)
@Configuration
public class RedisConfig {
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory f) {
return new StringRedisTemplate(f);
}
}
八、如何验证
- 并发压测两个请求同时调用
@RedisLock
方法,观察只有一个进入临界区;另一个要么等待成功、要么超时失败。 - 人为延长业务耗时(
Thread.sleep
),观察续期 是否发生:在 Redis 中PTTL lock:...
始终大于 0。 - 杀掉进程:确认锁会在 TTL 到期后自动释放。