用 Redis 实现分布式锁(含自动续期与安全释放)详解

该文章已生成可运行项目,

用 Redis 实现分布式锁(含自动续期与安全释放)详解

在分布式系统中,多个服务实例可能同时操作共享资源(如库存扣减、订单生成),为保证数据一致性,必须使用 分布式锁。Redis 凭借其高性能和原子操作能力,成为实现分布式锁的常用选择。

本文将深入讲解如何用 Redis 实现一个 安全、可重入、带自动续期(Watchdog)、支持高可用 的分布式锁,并提供 Java 实现示例(含 Redisson 与原生 Lua 脚本两种方式)。


一、分布式锁的核心要求

要求说明
互斥性同一时间只有一个客户端能持有锁
可重入性同一线程可多次获取同一把锁
锁释放安全只能由加锁的客户端释放(防误删)
自动续期(Watchdog)防止业务执行时间超过锁过期时间
高可用支持主从、集群、哨兵模式
高性能加锁/释放速度快,不影响业务

二、基础实现:SET + NX + EX

最简单的分布式锁实现:

SET lock:order:12345 "client_1" NX EX 10
  • NX:key 不存在时才设置(保证互斥)
  • EX 10:10秒后自动过期(防死锁)
  • client_1:唯一客户端标识(用于释放时校验)

问题:无续期机制,业务执行超时会自动释放,导致并发。


三、安全释放锁:Lua 脚本防误删

直接 DEL 锁可能误删其他客户端的锁。应使用 Lua 脚本校验 value

✅ Lua 脚本(unlock.lua

-- KEYS[1] = lock key
-- ARGV[1] = client_id

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

Java 调用示例:

public boolean unlock(String lockKey, String clientId) {
    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<>();
    redisScript.setScriptText(script);
    redisScript.setResultType(Long.class);

    Long result = redisTemplate.execute(
        redisScript,
        Collections.singletonList(lockKey),
        clientId
    );

    return result != null && result == 1;
}

四、自动续期(Watchdog 机制)

如果业务执行时间超过锁过期时间,锁会被自动释放,导致多个客户端同时进入临界区。

解决方案:启动一个后台线程(Watchdog),每隔一段时间检查锁是否仍被持有,若持有则延长过期时间。

Watchdog 工作流程:

客户端A加锁(EX 30s)
     ↓
启动 Watchdog 线程(每10s检查一次)
     ↓
若锁仍存在且属于本客户端 → 执行 EXPIRE lock:xxx 30
     ↓
业务执行完成 → 取消续期 + 释放锁

五、完整实现方案对比

方案是否推荐说明
🟡 原生 SET + Lua⚠️ 基础可用需自行实现续期、可重入
🟢 Redisson(推荐)✅ 强烈推荐内置 Watchdog、可重入、公平锁等
🔴 Jedis + 自研❌ 不推荐容易出错,维护成本高

六、使用 Redisson 实现(推荐方案)

Redisson 提供了开箱即用的分布式锁,完美支持:

  • 可重入
  • 自动续期(Watchdog)
  • 公平锁、读写锁
  • 高可用(集群/哨兵)

1. 添加依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.24.1</version>
</dependency>

2. 获取锁并自动续期

@Autowired
private RedissonClient redissonClient;

public void doBusiness() {
    RLock lock = redissonClient.getLock("lock:order:12345");
    try {
        // 尝试加锁,最多等待10秒,上锁后30秒自动解锁
        boolean res = lock.tryLock(10, 30, TimeUnit.SECONDS);
        if (res) {
            try {
                // 执行业务逻辑(可能耗时较长)
                System.out.println("Locked! Doing business...");
                Thread.sleep(25000); // 模拟长任务
            } finally {
                lock.unlock(); // 自动取消续期 + 安全释放
            }
        } else {
            System.out.println("Failed to acquire lock");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

3. Redisson 的 Watchdog 原理

  • 默认锁过期时间:30s
  • Watchdog 每 10s 检查一次
  • 若客户端仍持有锁 → 自动 EXPIRE lock:xxx 30
  • 解锁时自动取消续期

✅ 无需担心业务超时,只要客户端存活,锁就不会被释放。


七、原生 Redis + Lua 实现(学习用)

如果你不想使用 Redisson,也可自行实现 Watchdog。

1. 加锁(SETNX + EX)

public boolean tryLock(String lockKey, String clientId, int expireSeconds) {
    String result = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, clientId, expireSeconds, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(result);
}

2. 启动 Watchdog 线程

private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private volatile boolean isLocked = false;

public void watchDog(String lockKey, String clientId, int expireSeconds) {
    isLocked = true;
    scheduler.scheduleAtFixedRate(() -> {
        if (isLocked) {
            // 只有当前客户端持有锁时才续期
            String current = redisTemplate.opsForValue().get(lockKey);
            if (clientId.equals(current)) {
                redisTemplate.expire(lockKey, expireSeconds, TimeUnit.SECONDS);
            }
        }
    }, expireSeconds / 3, expireSeconds / 3, TimeUnit.SECONDS);
}

3. 释放锁(Lua 脚本)

见前文 Lua 脚本实现。

4. 使用示例

String lockKey = "lock:order:12345";
String clientId = "client_" + Thread.currentThread().getId();

if (tryLock(lockKey, clientId, 30)) {
    try {
        watchDog(lockKey, clientId, 30); // 启动续期
        // 执行业务
    } finally {
        isLocked = false; // 停止续期
        unlock(lockKey, clientId); // 安全释放
    }
}

八、可重入锁实现思路

可重入锁需记录:

  • 当前持有线程
  • 重入次数

可用 Hash 结构实现:

# 锁结构
lock:order:12345
  field: client_1
  value: 2   # 重入次数

加锁时:

  • 若 key 不存在 → 设置 client_1:1
  • 若 key 存在且 field == client_1 → value +1
  • 否则失败

释放时:

  • value -1,为 0 时删除 key

九、最佳实践与注意事项

项目建议
🔐 安全释放必须使用 Lua 脚本校验 client_id
🕒 锁过期时间设置合理(如 10~30s),避免过长导致阻塞
🔄 自动续期使用 Redisson 或自研 Watchdog
🧩 可重入生产环境必须支持
🚫 阻塞操作锁内避免网络调用、sleep
📊 监控记录加锁失败、等待时间
🧹 异常处理确保 finally 中释放锁
📈 高可用使用 Redis 集群或哨兵

十、常见问题(FAQ)

Q1:Redis 主从切换会导致锁失效吗?

✅ 会!主节点加锁后未同步到从节点,主节点宕机,从节点升主,锁丢失。

解决方案

  • 使用 Redlock 算法(多个独立 Redis 实例)
  • 使用 ZooKeeperetcd 实现更安全的分布式锁
  • 多数场景下,Redisson + 主从已足够(牺牲 CAP 中的 CP)

Q2:Watchdog 占用资源吗?

✅ 占用少量 CPU 和连接,但可接受。Redisson 默认只在持有锁时启动。

Q3:能用 SETNX + DEL 吗?

❌ 不安全!DEL 可能误删其他客户端的锁。


十一、总结:Redis 分布式锁实现方案对比

方案是否可重入自动续期安全释放推荐度
SETNX + DEL
SETNX + Lua⚠️⭐⭐⭐
自研 Watchdog⚠️⭐⭐⭐⭐
Redisson⭐⭐⭐⭐⭐

结语
使用 Redis 实现分布式锁,强烈推荐使用 Redisson。它封装了复杂的细节(可重入、续期、安全释放),让你像使用本地锁一样操作分布式锁。

🔗 核心代码一句话:

RLock lock = redisson.getLock("myLock");
lock.tryLock(10, 30, TimeUnit.SECONDS);

简单、安全、高效,是生产环境的最佳实践

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值