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

相关推荐
····懂···6 分钟前
抢占先机,PostgreSQL 中级专家认证的职业跃迁
数据库·postgresql
GBASE17 分钟前
“G”术时刻:南大通用GBase 8c典型运维场景-扩缩容场景快速定位性能瓶颈
数据库
Elastic 中国社区官方博客35 分钟前
用于 UBI 的 Elasticsearch 插件:从搜索查询中分析用户行为
大数据·数据库·elasticsearch·搜索引擎·全文检索
小白不想白a39 分钟前
【MySQL安全】什么是SQL注入,怎么避免这种攻击:前端防护、后端orm框架、数据库白名单
数据库·sql·mysql·安全
大路谈数字化41 分钟前
Oracle 19C 在centos中安装操作步骤和说明
数据库·oracle
张彦峰ZYF1 小时前
联合索引全解析:一棵树,撑起查询的半边天
数据库·mysql
熏鱼的小迷弟Liu1 小时前
【MySQL】MySQL中锁有哪些?
数据库·mysql
我不是星海1 小时前
MySQL深度理解-MySQL锁机制
数据库·mysql
经典19921 小时前
从单体到分布式:解锁架构进化密码
分布式·架构
转身後 默落1 小时前
06.Redis 配置文件说明
数据库·redis·bootstrap