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的这套实现,分布式锁在保证安全性的同时,实现了高性能和高可用性,是生产环境的首选方案。

相关推荐
小陈工1 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
科技小花5 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸5 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain5 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希6 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神6 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员6 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java6 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿7 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴7 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存