Skip to content

Redis 分布式锁

  分布式锁是微服务架构中解决资源竞争的核心手段。Redis 凭借高性能和原子操作,成为最常用的分布式锁实现方案之一。

一、为什么需要分布式锁?

单机环境(synchronized / ReentrantLock):
  ┌──────────┐
  │  JVM 1   │
  │  synchronized(lock) { ... }
  │  一个 JVM 内有效
  └──────────┘

分布式环境(多个 JVM):
  ┌──────────┐  ┌──────────┐  ┌──────────┐
  │  JVM 1   │  │  JVM 2   │  │  JVM 3   │
  │ 线程 A    │  │ 线程 B    │  │ 线程 C    │
  └────┬─────┘  └────┬─────┘  └────┬─────┘
       │             │             │
       └─────────────┼─────────────┘


              ┌──────────────┐
              │    Redis     │
              │ 分布式锁      │
              │ 跨 JVM 互斥   │
              └──────────────┘

二、分布式锁演进

2.1 第一版:SETNX + DEL

java
// 获取锁
public boolean tryLock(String key, String value, long expire) {
    return redis.setnx(key, value) == 1;
}

// 释放锁
public void unlock(String key) {
    redis.del(key);
}

问题:

  • 没有设置过期时间,如果获取锁的线程挂了,锁永远不会释放(死锁)
  • 如果线程 A 执行时间过长,锁过期,线程 B 获取到锁,线程 A 释放锁时可能误删 B 的锁

2.2 第二版:SETNX + EXPIRE(原子操作)

java
// 获取锁 + 设置过期时间(原子操作)
public boolean tryLock(String key, String value, long expire) {
    // SET key value NX EX expireSeconds
    return redis.set(key, value, "NX", "EX", expire) != null;
}

// 释放锁(对比 value 防止误删)
public void unlock(String key, String value) {
    String currentValue = redis.get(key);
    if (value.equals(currentValue)) {
        redis.del(key);
    }
}

问题: 释放锁的"判断 + 删除"不是原子操作。如果判断通过后、删除前,锁恰好过期,另一个线程获取到锁,仍然会误删。

2.3 第三版:Lua 脚本原子释放

java
// 释放锁(Lua 脚本保证原子性)
String script = 
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "    return redis.call('del', KEYS[1]) " +
    "else " +
    "    return 0 " +
    "end";

public void unlock(String key, String value) {
    redis.eval(script, Collections.singletonList(key), 
               Collections.singletonList(value));
}

问题: 锁过期时间难以确定。如果业务执行时间超过过期时间,锁会被释放,其他线程可以获取锁,出现并发问题。

2.4 第四版:Redisson 看门狗

java
// Redisson 自动续期
RLock lock = redisson.getLock("lock:order:1001");

// 方式一:自动续期(看门狗)—— 默认 30 秒,每 10 秒续期
lock.lock();
try {
    // 业务逻辑,即使执行时间超过 30 秒,锁也会自动续期
} finally {
    lock.unlock();
}

// 方式二:指定过期时间(不续期)
lock.lock(10, TimeUnit.SECONDS);

看门狗机制:

lock() → 获取锁成功 → 启动看门狗(每 10 秒续期到 30 秒)


执行完成 → unlock() → 取消看门狗 → 删除锁

如果进程崩溃 → 看门狗停止 → 30 秒后自动过期 → 锁自动释放

三、Redisson 完整示例

3.1 依赖

xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.0</version>
</dependency>

3.2 配置

yaml
spring:
  redis:
    host: localhost
    port: 6379

3.3 使用

java
@Service
public class OrderService {

    @Autowired
    private RedissonClient redisson;

    public void createOrder(Long orderId) {
        RLock lock = redisson.getLock("lock:order:" + orderId);
        
        try {
            // 尝试加锁,最多等待 10 秒,锁有效期 30 秒
            if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                // 业务逻辑
                createOrderInternal(orderId);
            } else {
                throw new BusinessException("获取锁失败");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

3.4 可重入锁

java
// Redisson 的 RLock 支持可重入
public void methodA() {
    RLock lock = redisson.getLock("lock:order:1001");
    lock.lock();
    try {
        methodB();  // 同一线程再次获取同一把锁,不会阻塞
    } finally {
        lock.unlock();
    }
}

public void methodB() {
    RLock lock = redisson.getLock("lock:order:1001");
    lock.lock();
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
}

3.5 公平锁、读写锁、联锁

java
// 公平锁(按请求顺序获取)
RLock fairLock = redisson.getFairLock("lock:fair");

// 读写锁(读读不互斥,读写互斥,写写互斥)
RReadWriteLock rwLock = redisson.getReadWriteLock("lock:rw");
rwLock.readLock().lock();   // 读锁
rwLock.writeLock().lock();  // 写锁

// 联锁(多个锁同时获取,同时释放)
RLock lock1 = redisson.getLock("lock:1");
RLock lock2 = redisson.getLock("lock:2");
RLock multiLock = redisson.getMultiLock(lock1, lock2);
multiLock.lock();

四、RedLock 红锁

4.1 单节点 Redis 锁的问题

单节点 Redis 锁的风险:
  1. 线程 A 获取 Master 上的锁
  2. Master 宕机,锁数据还没同步到 Slave
  3. Slave 提升为 Master(锁数据丢失)
  4. 线程 B 在新 Master 上获取同一把锁
  5. 线程 A 和 B 同时持有锁 → 并发问题

4.2 RedLock 原理

RedLock 使用 N 个独立的 Redis 节点(奇数个,如 5 个):

  1. 线程 A 向 N 个节点依次获取锁
  2. 如果超过 N/2+1 个节点获取成功 → 获得锁
  3. 锁的有效时间 = 初始时间 - 获取锁消耗时间
  4. 释放锁时向所有节点释放

  优势:即使少数节点宕机,只要多数节点存活,锁仍然可靠

4.3 RedLock 争议

RedLock 存在争议。Martin Kleppmann(《数据密集型应用》作者)认为 RedLock 在某些极端情况下不安全。实际项目中,大多数场景使用单节点 + 哨兵/集群即可满足需求,真正需要 RedLock 的场景很少。

五、最佳实践

java
// 完整的分布式锁最佳实践
public void executeWithLock(String lockKey, Runnable task) {
    RLock lock = redisson.getLock(lockKey);
    try {
        // 尝试获取锁:等待 5 秒,锁有效期 30 秒
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            try {
                task.run();
            } finally {
                // 只有当前线程持有锁才释放
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        } else {
            // 获取锁失败,走降级逻辑
            throw new LockAcquireException("获取锁超时");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new LockAcquireException("获取锁被中断");
    }
}

六、面试要点

Q1:Redis 分布式锁的实现原理?

  核心是 SET key value NX EX expire 原子命令。加锁成功返回 OK,失败返回 nil。释放锁时通过 Lua 脚本原子判断 value 是否匹配,匹配才删除,防止误删。

Q2:Redisson 的看门狗机制是什么?

  如果 lock() 未指定过期时间,Redisson 自动启动看门狗,每 10 秒续期到 30 秒。只要业务不主动释放,锁就不会过期。进程崩溃时看门狗停止,锁 30 秒后自动过期。

Q3:Redis 分布式锁有哪些问题?

  单节点宕机导致锁丢失(RedLock 解决)、锁过期时间难以确定(看门狗解决)、释放锁非原子操作(Lua 脚本解决)。生产环境推荐使用 Redisson 封装好的锁。

Q4:RedLock 是什么?有必要用吗?

  RedLock 是 Redis 作者提出的多节点分布式锁方案,需要在 N 个独立 Redis 节点上获取锁,获得 N/2+1 个才算成功。RedLock 存在理论争议,大多数场景单节点 + 哨兵/集群的锁已足够。真正需要 RedLock 的场景极少。