5、生产Redis高并发分布式锁实战

一、核心问题与解决方案

问题本质

ClientA Redis ClientB SET lock_key clientA_id EX 30 业务处理中... 锁超时自动释放 SET lock_key clientB_id EX 30 DEL lock_key 锁被意外释放! ClientA Redis ClientB

Redisson解决方案

  1. 客户端唯一ID:UUID+线程ID作为锁标识
  2. Lua脚本原子操作:校验+删除一步完成
  3. 看门狗机制:后台线程定期续期
  4. 发布订阅:锁释放通知避免无效轮询

二、源码实现解析

1. 加锁流程(Lock操作)

核心Lua脚本
lua 复制代码
-- KEYS[1]: 锁key
-- ARGV[1]: 锁过期时间(毫秒)
-- ARGV[2]: 客户端唯一标识(UUID:threadId)

-- 锁不存在时加锁
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1) -- 使用hash存储
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end

-- 锁存在且是当前线程持有(重入锁)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1) -- 重入计数+1
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end

-- 返回锁剩余生存时间
return redis.call('pttl', KEYS[1])
看门狗启动逻辑(RedissonLock类)
java 复制代码
private void scheduleExpirationRenewal(long threadId) {
    // 创建定时任务
    Timeout task = commandExecutor.getConnectionManager()
        .newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) {
                // 检查锁是否仍被当前线程持有
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        // 异常处理
                        return;
                    }
                    
                    if (res) {
                        // 递归调用,实现周期性续期
                        scheduleExpirationRenewal(threadId);
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 默认30s/3=10s执行一次
}
续期Lua脚本
lua 复制代码
-- KEYS[1]: 锁key
-- ARGV[1]: 续期时间(默认30s)
-- ARGV[2]: 客户端唯一标识

-- 检查是否仍是当前线程持有锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 续期
    redis.call('pexpire', KEYS[1], ARGV[1])
    return 1
end
return 0

2. 解锁流程(Unlock操作)

解锁Lua脚本
lua 复制代码
-- KEYS[1]: 锁key
-- KEYS[2]: 发布订阅频道
-- ARGV[1]: 释放锁消息(0L)
-- ARGV[2]: 锁过期时间
-- ARGV[3]: 客户端唯一标识

-- 锁不存在(可能已过期)
if (redis.call('exists', KEYS[1]) == 0) then
    -- 发布解锁消息
    redis.call('publish', KEYS[2], ARGV[1])
    return 1
end

-- 非当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil
end

-- 减少重入计数
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1)
if (counter > 0) then
    -- 重入次数>0,仅更新过期时间
    redis.call('pexpire', KEYS[1], ARGV[2])
    return 0
else
    -- 完全释放锁
    redis.call('del', KEYS[1])
    -- 发布解锁消息
    redis.call('publish', KEYS[2], ARGV[1])
    return 1
end
锁释放通知机制
java 复制代码
// RedissonLock.unlockAsync方法
protected RFuture<Boolean> unlockAsync(long threadId) {
    // 执行解锁Lua脚本
    RFuture<Boolean> future = unlockInnerAsync(threadId);
    
    future.onComplete((res, e) -> {
        // 取消看门狗任务
        cancelExpirationRenewal(threadId);
        
        if (e != null) {
            // 异常处理
            return;
        }
        
        if (res == null) {
            // 锁非当前线程持有
            throw new IllegalMonitorStateException();
        }
    });
    return future;
}

3. 锁等待机制

请求线程(Thread B) Redis服务器 发布订阅频道 锁持有线程(Thread A) 初始状态:Thread A持有锁 尝试获取锁 (lua脚本) 返回锁剩余TTL(500ms) 订阅锁释放频道 (SUBSCRIBE) 等待指定时间(500ms) 收到锁释放消息 重新尝试获取锁 直接尝试获取锁 alt [等待期间收到通知] [等待超时未收到通知] loop [等待锁释放] 取消订阅 (UNSUBSCRIBE) 执行业务逻辑 取消订阅 (UNSUBSCRIBE) 返回获取锁失败 alt [获取锁成功] [获取锁失败] Thread A释放锁 执行解锁lua脚本 发布锁释放消息(PUBLISH) 请求线程(Thread B) Redis服务器 发布订阅频道 锁持有线程(Thread A)

三、完整工作流程

加锁流程

是 否 尝试加锁 成功? 启动看门狗线程 获取锁剩余TTL 订阅锁释放频道 等待TTL时间 执行业务逻辑

解锁流程

是 是 否 否 执行解锁Lua脚本 校验通过? 减少重入计数 计数=0? 删除锁并发布通知 更新过期时间 抛出异常 取消看门狗任务

四、关键设计亮点

  1. 可重入锁设计

    • 使用Hash结构存储 clientId:重入次数
    • 避免同一线程多次加锁导致死锁
  2. 锁续命机制

    • 默认每10秒续期一次(internalLockLeaseTime/3)
    • 续期时间可配置(默认30秒)
  3. 高效等待机制

    java 复制代码
    // 订阅锁释放通知
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
        // 超时处理
    }
  4. 容错处理

    • 网络异常时自动重试
    • 加锁超时自动取消
    • 看门狗线程异常自动终止

五、最佳实践建议

  1. 锁命名规范

    java 复制代码
    // 使用业务前缀+资源ID
    RLock lock = redisson.getLock("order:pay:" + orderId);
  2. 超时时间设置

    java 复制代码
    // 根据业务最大耗时设置
    lock.lock(10, TimeUnit.SECONDS);
  3. 避免长事务

    • 超过30秒的业务考虑拆分
    • 监控看门狗日志,警惕续期失败
  4. 集群环境特别配置

    java 复制代码
    Config config = new Config();
    config.useClusterServers()
          .setCheckSlotsCoverage(false); // 避免slot覆盖检查

六、性能优化点

  1. 减少网络往返

    • 所有关键操作用Lua脚本实现
    • 单次请求完成多个操作
  2. 避免无效轮询

    • 通过发布订阅通知等待线程
    • 精确控制重试时机
  3. 轻量级看门狗

    • 定时任务而非持续轮询
    • 空闲时自动释放资源

通过Redisson的这套实现,分布式锁在保证安全性的同时,实现了高性能和高可用性,是生产环境的首选方案。

相关推荐
Elastic 中国社区官方博客2 小时前
在 Elasticsearch 中使用 Mistral Chat completions 进行上下文工程
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
编程爱好者熊浪3 小时前
两次连接池泄露的BUG
java·数据库
cr7xin4 小时前
缓存三大问题及解决方案
redis·后端·缓存
爱怪笑的小杰杰5 小时前
浏览器端缓存地图请求:使用 IndexedDB + ajax-hook 提升地图加载速度
ajax·okhttp·缓存
TDengine (老段)5 小时前
TDengine 字符串函数 CHAR 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
qq7422349845 小时前
Python操作数据库之pyodbc
开发语言·数据库·python
姚远Oracle ACE5 小时前
Oracle 如何计算 AWR 报告中的 Sessions 数量
数据库·oracle
Dxy12393102166 小时前
MySQL的SUBSTRING函数详解与应用
数据库·mysql
码力引擎6 小时前
【零基础学MySQL】第十二章:DCL详解
数据库·mysql·1024程序员节
杨云龙UP6 小时前
【MySQL迁移】MySQL数据库迁移实战(利用mysqldump从Windows 5.7迁至Linux 8.0)
linux·运维·数据库·mysql·mssql