Skip to content

分布式锁实现

  分布式锁是微服务架构中最基础也最容易出错的组件——在多个进程/服务之间实现互斥访问共享资源。单机时代用 synchronizedReentrantLock,但这些锁只对同一个 JVM 有效,多个服务实例之间互不可见。

一、什么是分布式锁

单机锁(synchronized / ReentrantLock):

  服务 A 唯一实例,部署在一台机器上
  ┌─────────────────────┐
  │ synchronized (lock) {│
  │   扣库存();           │  ← 只有一个 JVM,锁有效
  │ }                    │
  └─────────────────────┘

  问题:把服务 A 部署 3 个实例后,synchronized 只管自己 JVM 内部的线程,
        管不了其他实例。3 个实例各自有各自的锁,互不干扰 = 锁失效。


分布式锁:

  服务 A 实例 1  ──→ 抢锁 ──→ 拿到锁,扣库存
  服务 A 实例 2  ──→ 抢锁 ──→ 没拿到,等待/重试
  服务 A 实例 3  ──→ 抢锁 ──→ 没拿到,等待/重试
  
  锁不在 JVM 里,而在一个所有实例都能访问的"第三方"(Redis / ZK / Etcd / DB)

二、分布式锁解决了什么问题

  核心问题:多个服务实例同时操作同一份共享资源,导致数据不一致。

2.1 典型场景

场景没有分布式锁有分布式锁
秒杀扣库存100 件库存,卖出 200 件(超卖)同一时刻只有 1 个实例操作库存
重复下单用户手快点了 2 次,生成 2 个订单同一用户加锁,第二次请求被拦截
定时任务3 个实例同时执行,发了 3 封相同邮件只有抢到锁的实例执行
幂等控制网络超时重试,重复扣款锁 + 唯一标识防止重复执行
资源分配2 个用户同时抢到同一个优惠券抢锁成功后扣减,先到先得

2.2 本质

分布式锁 = 把"互斥"这个能力从 JVM 内部搬到 JVM 外部

  单机:锁在 JVM 堆内存中,只有同一进程内的线程可见
  分布式:锁在 Redis/ZK/Etcd 中,所有进程都能看到同一把锁

  这就是为什么 synchronized 在微服务中失效——
  不是 synchronized 有问题,而是锁的作用域变了。

三、分布式锁的五个必要条件

条件说明反例
互斥性同一时刻只有一个客户端持有锁两个实例同时扣库存 → 超卖
防死锁持有锁的客户端崩溃后,锁能被自动释放客户端挂了,锁永远不释放
解铃还须系铃人只有持有锁的客户端才能释放锁客户端 A 的锁被客户端 B 误删
可重入同一个客户端可以多次获取同一把锁递归调用时死锁
高可用锁服务本身不能是单点故障Redis 单节点挂了,锁全失效

三、Redis 实现分布式锁

3.1 错误示范:这样做锁不住

java
// ❌ 错误写法:两步操作不是原子的
public boolean wrongLock(String lockKey) {
    String result = redisTemplate.opsForValue().get(lockKey);  // ① GET
    if (result != null) {
        return false;  // 锁已被占用
    }
    // ② SET —— ①和②之间有并发窗口
    redisTemplate.opsForValue().set(lockKey, "1", Duration.ofSeconds(30));
    return true;
}
并发场景下,两个请求同时执行:

  请求 1:GET lock:order → null(锁不存在)
  请求 2:GET lock:order → null(锁不存在,请求1还没执行SET)
  请求 1:SET lock:order → 成功
  请求 2:SET lock:order → 成功(覆盖了请求1的锁!)
  
  两个请求都认为自己拿到了锁 💥

3.2 正确版:SETNX 一条命令搞定

加锁命令(Redis 2.6.12+):

  SET lock:order:1001 uuid-12345 NX PX 30000
       │                         │   │  │
       │                         │   │  └── 过期时间 30000 毫秒(30 秒,防死锁)
       │                         │   └── NX = Not eXists(不存在才设置,保证互斥)
       │                         └── 唯一标识(知道是谁加的锁,解铃还须系铃人)
       └── 锁的 key

  返回 OK    → 加锁成功
  返回 nil   → 锁已被其他客户端持有
java
/**
 * 基础版 Redis 分布式锁——正确但只适合学习
 * 生产环境请用 Redisson(3.4 节)
 */
public class SimpleRedisLock implements AutoCloseable {

    private final StringRedisTemplate redisTemplate;
    private final String lockKey;
    private final String lockValue;

    public SimpleRedisLock(StringRedisTemplate redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey;
        // value = UUID + 线程ID,确保每个锁持有者身份唯一
        this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
    }

    /**
     * 加锁
     * @param expireSeconds 锁过期时间(秒),防止死锁
     * @return true=加锁成功 false=锁已被占用
     */
    public boolean tryLock(int expireSeconds) {
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(expireSeconds));
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁——Lua 脚本保证原子性(判断身份 + 删除)
     */
    public void unlock() {
        String script =
            "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('DEL', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        redisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue);
    }

    @Override
    public void close() {
        unlock();
    }
}

// 使用 try-with-resources 自动释放
try (SimpleRedisLock lock = new SimpleRedisLock(redisTemplate, "lock:order:1001")) {
    if (lock.tryLock(30)) {
        deductStock();
    } else {
        throw new RuntimeException("系统繁忙,请稍后重试");
    }
}

3.3 为什么释放锁必须用 Lua

❌ 两步操作不是原子的——会发生误删:

  时刻 1:客户端 A 执行 GET lock:order → 返回 uuid-A(匹配,是我的锁)
  时刻 2:锁刚好过期,Redis 自动删除了锁
  时刻 3:客户端 B 执行 SET lock:order uuid-B NX PX 30000 → 拿到了锁
  时刻 4:客户端 A 执行 DEL lock:order → 删了客户端 B 的锁!💥
  
  结果:客户端 B 以为自己持有锁,实际上锁已经被 A 删了,
        C 又可以拿到锁 → A、B、C 可能同时操作共享资源


✅ Lua 原子操作——判断和删除在一条命令中完成:

  EVAL "
    if redis.call('GET', KEYS[1]) == ARGV[1] then
      return redis.call('DEL', KEYS[1])
    else
      return 0
    end
  " 1 lock:order:1001 uuid-12345

  Redis 单线程执行 Lua 脚本,整个脚本执行期间不会插入其他命令,
  所以"判断 + 删除"绝对不会被打断。

3.4 锁过期问题与看门狗

核心矛盾:锁过期时间不好设置

  设置 5 秒  → 业务执行了 6 秒 → 锁提前释放 → 其他线程拿到锁 → 并发问题
  设置 60 秒 → 业务 1 秒就执行完了,但锁被占 60 秒 → 其他线程白白等待 → 性能差
  设置 60 秒 → 业务执行到 5 秒时,服务突然 GC 停顿 10 秒 → 锁过期 → 同上

看门狗(Watchdog)机制:

  加锁成功 → 启动看门狗线程
  每 1/3 过期时间检查一次:业务线程还活着吗?
  活着 → 续期锁(重置过期时间)→ 继续等待
  业务完成 → 释放锁 → 停止看门狗
java
public class RedisLockWithWatchdog implements AutoCloseable {

    private final StringRedisTemplate redisTemplate;
    private final String lockKey;
    private final String lockValue;
    private ScheduledExecutorService watchdog;
    private volatile boolean locked = false;

    public RedisLockWithWatchdog(StringRedisTemplate redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey;
        this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
    }

    public boolean tryLock(int expireSeconds) {
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(expireSeconds));
        if (Boolean.TRUE.equals(success)) {
            locked = true;
            startWatchdog(expireSeconds);
        }
        return Boolean.TRUE.equals(success);
    }

    private void startWatchdog(int expireSeconds) {
        watchdog = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "watchdog-" + lockKey);
            t.setDaemon(true);  // 守护线程,主线程退出时自动结束
            return t;
        });

        int renewInterval = expireSeconds / 3;  // 每 1/3 过期时间续期
        watchdog.scheduleAtFixedRate(() -> {
            if (!locked) {
                watchdog.shutdown();
                return;
            }
            // 续期:只有当 value 匹配时才续期(防止误续别人的锁)
            String script =
                "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
                "    return redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
                "else return 0 end";
            Long result = redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(lockKey),
                lockValue, String.valueOf(expireSeconds));
            if (result == null || result == 0) {
                locked = false;  // 续期失败,锁可能已被其他人持有
                watchdog.shutdown();
            }
        }, renewInterval, renewInterval, TimeUnit.SECONDS);
    }

    public void unlock() {
        locked = false;
        if (watchdog != null && !watchdog.isShutdown()) {
            watchdog.shutdown();
        }
        String script =
            "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('DEL', KEYS[1]) " +
            "else return 0 end";
        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(lockKey), lockValue);
    }

    @Override
    public void close() { unlock(); }
}

3.5 Redisson:生产级 Redis 分布式锁

  手写分布式锁容易出错,Redisson 是目前最成熟的 Redis Java 客户端,内置了分布式锁全家桶。

3.5.1 快速上手

xml
<!-- Spring Boot 3.x 最新依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.37.0</version>
</dependency>
yaml
# application.yml
spring:
  redis:
    redisson:
      config: |
        singleServerConfig:
          address: "redis://127.0.0.1:6379"
          connectionPoolSize: 64
          connectionMinimumIdleSize: 24
java
@Service
public class OrderService {

    @Autowired
    private RedissonClient redissonClient;

    public void deductStock(String orderId) {
        RLock lock = redissonClient.getLock("lock:order:" + orderId);

        try {
            // tryLock(等待时间, 锁过期时间, 时间单位)
            // 等待 10 秒拿不到锁就放弃,拿到锁后 30 秒自动释放
            if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                try {
                    doDeductStock(orderId);
                } finally {
                    // 重要:判断锁是否仍被当前线程持有
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                    }
                }
            } else {
                throw new RuntimeException("系统繁忙,请稍后重试");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁被中断", e);
        }
    }
}

3.5.2 Redisson 看门狗机制详解

Redisson 看门狗默认行为:

  加锁时没有指定 leaseTime(过期时间):
    → 默认锁 30 秒过期
    → 看门狗每 10 秒续期一次,续期到 30 秒
    → 业务执行多久,锁就持有多久,不会提前释放

  加锁时指定了 leaseTime:
    → 锁按指定时间过期
    → 看门狗不启动
    → 到时间自动释放,不管业务是否执行完

  // 看门狗模式(推荐):不指定过期时间,让 Redisson 自动管理
  lock.tryLock(10, TimeUnit.SECONDS);  // 只指定等待时间

  // 固定时间模式:指定过期时间,看门狗不工作
  lock.tryLock(10, 30, TimeUnit.SECONDS);  // 等待 + 过期都指定
Redisson 看门狗内部实现(源码级):

  1. 加锁时,Redisson 在 Redis 中写入一个 Hash:
     HSET lock:order:1001 <uuid:threadId> 1
     PEXPIRE lock:order:1001 30000

  2. 启动看门狗定时任务(基于 Netty 的 HashedWheelTimer,性能极高)
     每 internalLockLeaseTime / 3 = 10 秒执行一次

  3. 续期时执行 Lua 脚本:
     if redis.call('HEXISTS', KEYS[1], ARGV[2]) == 1 then
       return redis.call('PEXPIRE', KEYS[1], ARGV[1])
     else
       return 0
     end

  4. unlock() 时取消看门狗任务

3.5.3 Redisson 可重入锁原理

java
// 可重入:同一个线程可以多次获取同一把锁
public void methodA() {
    RLock lock = redissonClient.getLock("lock:order:1001");
    lock.lock();
    try {
        methodB();  // 内部再次获取同一把锁
    } finally {
        lock.unlock();
    }
}

public void methodB() {
    RLock lock = redissonClient.getLock("lock:order:1001");
    lock.lock();  // 不会死锁,因为是同一个线程
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
}
Redisson 可重入锁的数据结构(Hash):

  key: lock:order:1001
  field: <uuid:threadId>   → value: 2  (重入计数)
  
  第一次 lock() → value = 1
  第二次 lock() → value = 2
  第一次 unlock() → value = 1
  第二次 unlock() → value = 0 → 删除 key,释放锁

3.5.4 Redisson 锁类型全景

锁类型接口特点适用场景
可重入锁RLock同一线程可多次获取大多数场景,默认选择
公平锁RFairLock按请求顺序排队获取需要公平排队,避免饥饿
读写锁RReadWriteLock读读不互斥,读写互斥读多写少(如配置读取)
联锁RMultiLock多把锁同时获取/释放需要同时锁定多个资源
红锁RedissonRedLock多节点过半确认高可用,防止主从丢锁
自旋锁RLock + 自旋循环重试不阻塞锁持有时间极短(< 1ms)
信号量RSemaphore限制并发访问数限流、连接池控制
java
// 公平锁:按请求顺序排队
RFairLock fairLock = redissonClient.getFairLock("lock:order:1001");
fairLock.lock();

// 读写锁:读读并发,读写互斥
RReadWriteLock rwLock = redissonClient.getReadWriteLock("lock:config");
// 读锁(多个线程可以同时持有)
RLock readLock = rwLock.readLock();
readLock.lock();
// 写锁(独占)
RLock writeLock = rwLock.writeLock();
writeLock.lock();

// 联锁:同时锁定多个资源
RLock lock1 = redissonClient.getLock("lock:order:1001");
RLock lock2 = redissonClient.getLock("lock:user:1001");
RMultiLock multiLock = redissonClient.getMultiLock(lock1, lock2);
multiLock.lock();  // 两把锁同时获取,全部成功才算成功

// 信号量:限制并发数
RSemaphore semaphore = redissonClient.getSemaphore("semaphore:task");
semaphore.trySetPermits(10);  // 最多 10 个并发
semaphore.acquire();           // 获取一个许可
try { doWork(); }
finally { semaphore.release(); }

3.5.5 AOP 注解式分布式锁(生产推荐)

java
/**
 * 自定义注解:一行注解搞定分布式锁
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {

    /** 锁的 key,支持 SpEL 表达式 */
    String key();

    /** 锁前缀 */
    String prefix() default "lock:";

    /** 等待获取锁的时间(秒),-1 表示不等待 */
    long waitTime() default 3;

    /** 锁过期时间(秒),-1 表示使用看门狗 */
    long leaseTime() default -1;

    /** 获取锁失败时的提示信息 */
    String failMessage() default "系统繁忙,请稍后重试";
}
java
/**
 * 分布式锁 AOP 切面
 */
@Aspect
@Component
@Order(1)  // 确保在 @Transactional 之前执行
public class RedisLockAspect {

    @Autowired
    private RedissonClient redissonClient;

    private final ExpressionParser parser = new SpelExpressionParser();
    private final DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

    @Around("@annotation(redisLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
        // 解析 SpEL 表达式生成锁 key
        String lockKey = parseKey(redisLock.key(), redisLock.prefix(), joinPoint);

        RLock lock = redissonClient.getLock(lockKey);

        boolean acquired;
        if (redisLock.leaseTime() == -1) {
            // 看门狗模式
            acquired = lock.tryLock(redisLock.waitTime(), TimeUnit.SECONDS);
        } else {
            acquired = lock.tryLock(redisLock.waitTime(), redisLock.leaseTime(), TimeUnit.SECONDS);
        }

        if (!acquired) {
            throw new RuntimeException(redisLock.failMessage());
        }

        try {
            return joinPoint.proceed();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private String parseKey(String keyExpr, String prefix, ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        EvaluationContext context = new StandardEvaluationContext();
        String[] paramNames = discoverer.getParameterNames(method);
        Object[] args = joinPoint.getArgs();

        if (paramNames != null) {
            for (int i = 0; i < paramNames.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
        }

        // 支持 SpEL:如 #orderId 会取方法参数 orderId 的值
        Expression expression = parser.parseExpression(keyExpr);
        String key = expression.getValue(context, String.class);
        return prefix + key;
    }
}
java
// 使用:一行注解即可
@Service
public class OrderService {

    @RedisLock(key = "#orderId", prefix = "lock:order:", waitTime = 5)
    public void deductStock(String orderId) {
        // 业务逻辑:自动加锁,方法执行完自动释放
        orderMapper.deductStock(orderId);
    }

    @RedisLock(key = "#userId + ':' + #productId", waitTime = 3, failMessage = "请勿重复下单")
    public void createOrder(Long userId, Long productId) {
        // 自动加锁 + 自动释放
    }
}

3.6 RedLock:多节点 Redis 高可用方案

问题:主从架构下,主节点挂了,锁可能丢失

  客户端 A 在主节点加锁成功
  主节点还没来得及同步到从节点就挂了
  哨兵将从节点提升为主节点(锁数据丢失)
  客户端 B 在新主节点加锁成功
  → 客户端 A 和 B 同时持有锁!💥

RedLock 方案(至少 3 个独立 Redis 节点,非主从关系):

  客户端 A:
    1. 获取当前时间戳 T1
    2. 依次向 Node1、Node2、Node3 请求加锁
       - 每个节点超时时间远小于锁过期时间(如 50ms)
       - 失败不阻塞,立即尝试下一个
    3. 获取当前时间戳 T2
    4. 成功条件:
       ✅ 超过半数节点(N/2 + 1)加锁成功
       ✅ 总耗时 < 锁过期时间
    5. 如果成功,锁真实有效时间 = 锁过期时间 - (T2 - T1)
    6. 如果失败,向所有节点发起释放
java
// Redisson 的 RedLock 实现
RLock lock1 = redissonClient1.getLock("lock:order:1001");
RLock lock2 = redissonClient2.getLock("lock:order:1001");
RLock lock3 = redissonClient3.getLock("lock:order:1001");

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
    if (redLock.tryLock(10, 30, TimeUnit.SECONDS)) {
        try {
            deductStock();
        } finally {
            redLock.unlock();
        }
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

RedLock 争议(Martin Kleppmann vs antirez 论战): Martin 认为时钟跳跃(NTP 校时)可能导致锁过期判断错误;antirez 认为时钟跳跃是极端情况,且可以通过 fencing token 解决。实际生产建议: 绝大多数场景用 Redisson 单节点 + 哨兵就够了,真正需要 RedLock 的场景极少。如果对一致性要求极高,直接用 ZK 或 Etcd(CP 系统)。

四、ZooKeeper 实现分布式锁

4.1 原理:临时顺序节点

ZooKeeper 的临时顺序节点机制:

  节点类型:
  - 临时节点(EPHEMERAL):客户端断开连接 → 节点自动删除
  - 顺序节点(SEQUENTIAL):ZK 自动在节点名后追加递增序号

  加锁流程:
  ① 所有客户端在 /lock 下创建临时顺序节点
  ② 获取 /lock 下所有子节点,按序号排序
  ③ 如果自己是最小的序号 → 获取锁成功
  ④ 如果不是最小的 → 对前一个节点注册 Watcher(监听删除事件)
  ⑤ 前一个节点被删除 → 收到通知 → 重新检查自己是否最小

  /lock
  ├── /lock/_c_0000000001  ← 客户端 A(最小,持有锁)
  ├── /lock/_c_0000000002  ← 客户端 B(监听 0000000001)
  └── /lock/_c_0000000003  ← 客户端 C(监听 0000000002)

  客户端 A 释放锁(删除 0000000001)→ ZK 通知客户端 B
  → 客户端 B 成为最小的 → 获取锁 → 通知客户端 C 监听自己
ZK 锁的天然优势:

  ✅ 防死锁:客户端断开连接,临时节点自动删除,锁自动释放
  ✅ 公平性:顺序节点天然排队,按请求顺序获取锁
  ✅ 通知机制:Watcher 主动通知,不用轮询
  ✅ 强一致:ZK 是 CP 系统,不会出现锁丢失

4.2 Curator 实现(推荐)

xml
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.7.1</version>
</dependency>
java
@Configuration
public class CuratorConfig {

    @Bean
    public CuratorFramework curatorFramework() {
        CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .sessionTimeoutMs(60000)
            .connectionTimeoutMs(15000)
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))  // 重试策略
            .namespace("distributed-lock")  // 命名空间隔离
            .build();
        client.start();
        return client;
    }
}
java
@Service
public class OrderService {

    @Autowired
    private CuratorFramework curatorClient;

    public void deductStock(String orderId) throws Exception {
        // InterProcessMutex:可重入互斥锁
        InterProcessMutex lock = new InterProcessMutex(
            curatorClient, "/lock/order/" + orderId);

        try {
            if (lock.acquire(10, TimeUnit.SECONDS)) {
                try {
                    doDeductStock(orderId);
                } finally {
                    lock.release();
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("获取锁失败", e);
        }
    }
}

4.3 Curator 提供的锁类型

锁类型类名说明
互斥锁InterProcessMutex可重入互斥锁,最常用
读写锁InterProcessReadWriteLock读读不互斥,读写互斥
信号量InterProcessSemaphoreV2控制并发数,租约模式
多重锁InterProcessMultiLock同时管理多把锁

五、Etcd 实现分布式锁

  Etcd 是 CoreOS 开源的分布式 KV 存储,基于 Raft 共识算法,是 Kubernetes 的核心组件。Etcd 的 Lease(租约)+ 事务机制天然适合分布式锁,是 ZK 的现代替代方案。

5.1 原理

Etcd 分布式锁机制:

  ① 创建 Lease(租约),设置 TTL
  ② 以 Lease 的身份写入一个 key(如 /lock/order/1001)
  ③ 利用 Etcd 的事务(Transaction)保证原子性:
     - 如果 key 不存在 → 写入成功 → 获取锁
     - 如果 key 已存在 → 写入失败 → 锁被占用
  ④ 持有锁期间,定时续租(KeepAlive)
  ⑤ 释放锁:删除 key 或让 Lease 过期

5.2 Java 实现(jetcd)

xml
<dependency>
    <groupId>io.etcd</groupId>
    <artifactId>jetcd-core</artifactId>
    <version>0.7.8</version>
</dependency>
java
public class EtcdLock implements AutoCloseable {

    private final Client client;
    private final Lease leaseClient;
    private final KV kvClient;
    private final String lockKey;
    private long leaseId;
    private ScheduledExecutorService keepAliveExecutor;

    public EtcdLock(String endpoints, String lockKey) {
        this.client = Client.builder()
            .endpoints(endpoints.split(","))
            .build();
        this.leaseClient = client.getLeaseClient();
        this.kvClient = client.getKVClient();
        this.lockKey = lockKey;
    }

    /**
     * 加锁
     * @param ttlSeconds 租约 TTL(秒)
     */
    public boolean tryLock(long ttlSeconds) throws Exception {
        // ① 创建租约
        leaseId = leaseClient.grant(ttlSeconds).get().getID();

        // ② 启动续约
        keepAliveExecutor = Executors.newSingleThreadScheduledExecutor();
        keepAliveExecutor.scheduleAtFixedRate(() -> {
            try {
                leaseClient.keepAliveOnce(leaseId).get();
            } catch (Exception e) {
                // 续约失败,锁可能已失效
            }
        }, ttlSeconds / 3, ttlSeconds / 3, TimeUnit.SECONDS);

        // ③ 事务写入:key 不存在才写入
        ByteSequence key = ByteSequence.from(lockKey, StandardCharsets.UTF_8);
        ByteSequence value = ByteSequence.from(
            UUID.randomUUID().toString(), StandardCharsets.UTF_8);

        Txn txn = kvClient.txn()
            .If(new CmpOp(key, Cmp.Op.EQUAL, CmpTarget.createRevision(0)))  // revision=0 表示 key 不存在
            .Then(Op.put(key, value, PutOption.newBuilder().withLeaseId(leaseId).build()))
            .Else(Op.get(key));

        TxnResponse response = txn.commit().get();
        return response.isSucceeded();
    }

    public void unlock() {
        if (keepAliveExecutor != null) {
            keepAliveExecutor.shutdown();
        }
        if (leaseId != 0) {
            leaseClient.revoke(leaseId);
        }
    }

    @Override
    public void close() {
        unlock();
        client.close();
    }
}

六、四大方案全面对比

6.1 技术维度对比

维度RedisZooKeeperEtcd数据库
算法单线程 + 内存ZAB 协议Raft 协议行锁 / 唯一索引
一致性AP(最终一致)CP(强一致)CP(强一致)CP(强一致)
性能(QPS)10 万+~1 万~2 万~500
锁释放依赖过期时间临时节点自动删除租约自动过期依赖过期时间
公平性需手动实现天然支持需手动实现需手动实现
可重入Redisson 支持Curator 支持需手动实现需手动实现
运维成本最低
社区活跃度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
学习成本

6.2 生态对比

Redis 生态(最广泛):
  ✅ Redisson:功能最全,锁类型丰富,看门狗,AOP 注解
  ✅ Spring Integration:spring-integration-redis
  ✅ 几乎所有语言都有成熟客户端

ZooKeeper 生态(经典):
  ✅ Curator:Apache 官方高级 API,封装完善
  ⚠️ 年轻团队较少选择,逐步被 Etcd 替代

Etcd 生态(云原生):
  ✅ Kubernetes 标配,云原生生态首选
  ✅ jetcd:Java 官方客户端
  ⚠️ Java 生态成熟度不如 Redis

数据库(兜底):
  ⚠️ 性能最差,仅在没有 Redis/ZK/Etcd 时使用

七、选型决策树

你的场景                                  → 推荐方案
───────────────────────────────────────────────────────────────
最普遍:已有 Redis,高并发,允许最终一致    → Redisson(单节点 + 哨兵)
                                          → 默认看门狗模式,不指定 leaseTime

强一致性:金融、交易、对锁丢失零容忍         → ZK(Curator)或 Etcd
                                          → 基于 CP 共识算法,不会丢锁

公平排队:秒杀场景,需要严格按请求顺序       → ZK 顺序节点 或 Redisson RFairLock

云原生:K8s 环境,已有 Etcd               → Etcd(jetcd)
                                          → 不需要额外部署锁服务

已有 ZK,团队熟悉 ZK                     → Curator(不要新引入 ZK 仅为锁)

超高性能:锁持有时间 < 1ms,不允许等待       → Redisson 自旋锁 + 本地缓存

只有数据库,没有其他组件                   → 数据库唯一索引 + 定时清理
                                          → 仅限遗留系统,新项目不推荐

大规模分布式:100+ 实例,极高可用           → RedLock(3 节点 Redis)
                                          → 或 Etcd 集群(已内置 Raft 高可用)

八、Spring Boot 3.x 生产级配置

java
/**
 * 分布式锁工厂:统一管理 Redis 和 ZK 锁
 */
@Configuration
public class DistributedLockConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://127.0.0.1:6379")
            .setConnectionPoolSize(64)
            .setConnectionMinimumIdleSize(24)
            .setRetryAttempts(3)
            .setRetryInterval(1500)
            .setTimeout(3000)
            .setConnectTimeout(10000);
        return Redisson.create(config);
    }

    @Bean
    public CuratorFramework curatorFramework() {
        CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .sessionTimeoutMs(60000)
            .connectionTimeoutMs(15000)
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .namespace("distributed-lock")
            .build();
        client.start();
        return client;
    }
}

九、常见坑与解决方案

9.1 锁过期,业务没执行完

症状:数据库偶尔出现重复数据,但频率很低

原因:锁过期时间 30 秒,业务在 GC 停顿后执行了 35 秒,
      锁在 30 秒时自动释放,另一个实例拿到锁,两个实例同时操作

解决(按优先级):
  1. 使用 Redisson 看门狗模式(不指定 leaseTime)
  2. 优化业务逻辑,减少锁持有时间
  3. 数据库层加唯一索引兜底

9.2 主从切换丢锁

症状:Redis 主从切换后,短时间内出现重复操作

原因:主节点还没来得及同步锁数据到从节点就挂了

解决:
  1. 业务层做幂等兜底(数据库唯一索引)
  2. 使用 RedLock(3 节点独立 Redis,过半确认)
  3. 改用 ZK / Etcd(CP 系统,强一致)

9.3 可重入问题导致死锁

症状:递归调用或 AOP 嵌套时,线程卡死

  methodA() {
    lock.lock();        // 第一次加锁,成功
    methodB();          // 内部调用
  }
  methodB() {
    lock.lock();        // 第二次加锁,锁已被持有 → 不可重入锁 → 死锁
  }

解决:使用 Redisson RLock 或 Curator InterProcessMutex(都支持可重入)

9.4 锁粒度问题

❌ 粒度太粗——一把锁锁所有:
  lock:product → 所有商品共用一个锁 → 卖 A 商品时,B 商品也要排队

✅ 粒度合适——一个商品一把锁:
  lock:product:1001 → 每个商品独立锁 → 互不干扰

✅ 库存分片——秒杀场景进一步拆分:
  lock:product:1001:0 → 库存分片 0
  lock:product:1001:1 → 库存分片 1
  ... 
  用户 user_id % 10 → 路由到对应分片 → 并行度提升 10 倍

9.5 集群脑裂(Split-Brain)

Redis 集群网络分区:

  客户端 A ──→ Redis 节点 1 → 加锁成功
  客户端 B ──→ Redis 节点 2 → 认为节点 1 挂了,自己成为主
                              → 客户端 B 加锁成功
  → 两个客户端同时持有锁!

解决:
  1. Redis 集群配置:min-replicas-to-write 1  min-replicas-max-lag 10
  2. 使用 RedLock(过半确认)
  3. 业务层加 fencing token(单调递增的版本号)

十、分布式锁性能测试数据

测试环境:Redis 7.x 单节点,本地回环,3 个客户端并发

| 锁类型 | 加锁+释放 TPS | 平均延迟 | P99 延迟 |
|--------|-------------|---------|---------|
| Redisson RLock(看门狗) | ~25,000/s | 0.04ms | 0.5ms |
| Redisson RFairLock | ~8,000/s | 0.12ms | 2ms |
| Curator(ZK) | ~3,000/s | 0.3ms | 5ms |
| 手写 SETNX + Lua | ~50,000/s | 0.02ms | 0.2ms |
| 数据库唯一索引 | ~500/s | 2ms | 20ms |

结论:Redisson 的 RLock 性能足够 99% 的场景,手写 SETNX 只快 2 倍,
      但 Redisson 多了看门狗、可重入、公平锁、读写锁等能力。
      除非你确定需要极致性能且场景极其简单,否则直接用 Redisson。

十一、面试要点

Q1:Redis 分布式锁怎么实现?

  加锁:SET lock_key uuid NX PX 30000;释放:Lua 脚本判断 value 匹配后 DEL;续期:看门狗定时 RENEW。生产环境推荐用 Redisson,封装了看门狗、可重入、公平锁、读写锁等。

Q2:Redis 分布式锁释放时为什么要用 Lua?

  GET + DEL 是两步操作,不是原子的。如果 GET 之后、DEL 之前锁过期了,另一个客户端获取了锁,就会误删别人的锁。Lua 脚本将判断和删除合并为原子操作。

Q3:Redis 和 ZK 分布式锁怎么选?

  Redis 性能高(10 万+ QPS),适合高并发场景,但主从切换可能丢锁;ZK 强一致,适合金融、交易等场景,但性能较低(~1 万 QPS)。绝大多数互联网项目用 Redis + Redisson 就够了。

Q4:RedLock 解决了什么问题?有什么争议?

  解决 Redis 主从切换丢锁的问题。争议在于 Martin Kleppmann 认为时钟跳跃可能导致锁失效,antirez 认为实际场景中极少发生。生产环境用 Redisson 单节点 + 哨兵 + 业务幂等兜底更实际。

Q5:分布式锁和数据库唯一索引的关系?

  分布式锁是同步工具,唯一索引是数据约束。两者配合:分布式锁控制并发访问,唯一索引做最终兜底。即使锁失效了,唯一索引也能保证数据不重复。

Q6:看门狗是什么?为什么需要它?

  看门狗是自动续期机制。锁过期时间不好设置(太短业务没执行完,太长浪费性能),看门狗定时检查业务是否还在执行,在则续期,业务完成则停止。Redisson 默认开启,不指定 leaseTime 即可。

Q7:Etcd 和 ZK 在分布式锁上有什么区别?

  Etcd 基于 Raft,ZK 基于 ZAB,都是 CP 系统。Etcd 是 Go 写的,ZK 是 Java 写的;Etcd 是 K8s 标配,云原生生态更好;ZK 在 Java 生态更成熟(Curator 封装完善)。新项目偏向 Etcd。