分布式锁是分布式系统中保证多进程/多实例互斥访问共享资源的核心组件。最经典且生产中最常用的分布式锁实现是基于 Redis 的(RedLock 算法或单节点简化版)。

关键点:过期时间,等待时间,lock key 、lock value、重试时间、重试间隔、续期

Redisson 中的实现

redisson中的看门狗机制总结

Redisson分布式锁流程

分布式锁核心要素 & 关键参数

参数 推荐值 / 说明 为什么重要
lock_key 业务含义明确的 key,例如 lock:order:12345 唯一标识一把锁
lock_value 随机 UUID(最好是全局唯一,如 hostname + threadId + UUID.randomUUID() 用于安全释放锁,防止误删他人锁(A-B-A 问题)
过期时间(TTL) 业务最大执行时间 + 缓冲(如 10~30 秒) 防止死锁:即使客户端崩溃,锁也能自动过期
获取锁超时时间 通常等于或略大于 TTL(如 15~35 秒) 防止无限等待锁,快速失败
重试次数 3~10 次(视业务对延迟敏感度) 提高获取锁成功率
重试间隔(backoff) 指数退避 + 随机 jitter(如 100ms → 200ms → 400ms + random(0~100ms)) 防止惊群效应(thundering herd),避免大量客户端同时重试
续期时间(watchdog) 每 TTL/3 时间续期一次(例如 TTL=30s,每 10s 续期一次) 防止业务处理时间 > TTL 导致锁被别人抢走

Redis 关键命令(基于 Redisson 实现原理的手写版)

# 1. 获取锁(推荐使用 Lua 脚本原子性)
SET lock_key lock_value NX PX 30000
# NX:只有 key 不存在时才设置成功
# PX:毫秒级自动过期

# 2. 释放锁(必须用 Lua 脚本,保证检查 value + 删除 原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

# 3. 续期锁(同样用 Lua)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("pexpire", KEYS[1], ARGV[2])
else
    return 0
end

完整加锁流程(伪代码)

String lockValue = UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
long expireMs = 30_000L;
long acquireTimeout = 35_000L;
int retryTimes = 6;

long start = System.currentTimeMillis();

while (true) {
    // 尝试获取锁
    Boolean success = redisTemplate.execute(setNxPxScript,
                     Collections.singletonList(lockKey), lockValue, expireMs);

    if (success) {
        // 获取成功,开启自动续期线程(watchdog)
        startWatchDog(lockKey, lockValue, expireMs);
        return true;
    }

    // 重试判断
    if (System.currentTimeMillis() - start > acquireTimeout) {
        return false; // 超时失败
    }

    // 指数退避 + jitter
    long sleepMs = Math.min(100L << retryTimes, 1000) + random.nextInt(100);
    Thread.sleep(sleepMs);
    retryTimes--;
    if (retryTimes <= 0) return false;
}

自动续期(WatchDog)实现要点

private void startWatchDog(String key, String value, long ttl) {
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    executor.scheduleAtFixedRate(() -> {
        try {
            Long result = redisTemplate.execute(extendScript,
                            Collections.singletonList(key), value, ttl);
            if (result == null || result == 0) {
                // 续期失败,说明锁已不属于自己,停止续期
                executor.shutdown();
            }
        } catch (Exception e) {
            // 记录日志,继续尝试续期
        }
    }, ttl / 3, ttl / 3, TimeUnit.MILLISECONDS);

    // 在 finally 中 shutdown(释放锁时也要停止续期)
}

释放锁(必须安全释放)

public void unlock() {
    try {
        redisTemplate.execute(releaseScript, Collections.singletonList(lockKey), lockValue);
    } finally {
        if (watchDogExecutor != null) {
            watchDogExecutor.shutdownNow();
        }
    }
}

常见坑 & 最佳实践总结

  1. 永远使用随机 value + Lua 释放锁
  2. 必须开启续期(Redisson 默认开启),否则长任务会丢失锁
  3. 重试要加随机 jitter,防止大量实例同时重试