《7天学会Redis》特别篇: Redis分布式锁

有网友提了这几个需求,这里,我整理了特别篇发出来
《7天学会Redis》特别篇: 如何保证缓存与数据库的数据一致性?
《7天学会Redis》特别篇: Redis分布式锁

Redis分布式锁

1. Redis分布式锁的基础实现

1.1 使用SET命令实现

Redis分布式锁最基本且关键的实现方式是使用SET命令的NX(Not eXists)和PX(毫秒级过期时间)参数。这种实现方式简单直接,能够满足基本的分布式锁需求。

实现原理

  • NX参数:确保只在键不存在时设置值,实现了互斥性

  • PX参数:设置键的过期时间(毫秒),防止死锁

  • 唯一值:每个客户端生成唯一标识,防止误删他人锁

基本命令格式

bash 复制代码
SET lock_key unique_value NX PX 30000

工作流程图

基础实现代码示例

java 复制代码
public class SimpleRedisLock {
    private Jedis jedis;
    private String lockKey;
    private String clientId;
    
    public SimpleRedisLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.clientId = UUID.randomUUID().toString();
    }
    
    /**
     * 尝试获取锁
     * @param expireTime 锁过期时间(毫秒)
     * @return 是否获取成功
     */
    public boolean tryLock(long expireTime) {
        String result = jedis.set(lockKey, clientId, "NX", "PX", expireTime);
        return "OK".equals(result);
    }
}

1.2 解锁的原子性问题与Lua脚本

解锁操作不是简单的DEL命令,需要确保只有锁的持有者才能释放锁。非原子性的解锁操作可能导致严重问题。

非原子解锁的问题场景

解决方案:使用Lua脚本保证原子性

java 复制代码
public boolean unlock() {
    String luaScript = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "    return redis.call('del', KEYS[1]) " +
        "else " +
        "    return 0 " +
        "end";
    
    Long result = (Long) jedis.eval(luaScript, 
                    Collections.singletonList(lockKey), 
                    Collections.singletonList(clientId));
    
    return result == 1;
}

原子解锁的优势

  1. 操作原子性:读取、比较、删除在一个原子操作中完成

  2. 安全性:只有锁持有者才能释放锁

  3. 简单性:无需复杂的业务逻辑判断

1.3 存在的问题与改进

基础实现虽然简单,但存在一些明显问题:

主要问题

  1. 锁续期困难:业务执行时间超过锁过期时间时,无法自动续期

  2. 非可重入:同一线程无法多次获取同一把锁

  3. 锁粒度单一:缺乏读写分离等高级特性

  4. 单点故障:单个Redis实例故障时锁不可用

改进方向

  1. 实现锁续期机制:添加后台线程定期续期

  2. 支持可重入:记录锁持有者信息和重入次数

  3. 实现多种锁类型:读写锁、公平锁等

  4. 多实例支持:使用红锁算法提高可用性

2. Redisson分布式锁的高级实现

2.1 Redisson概述

Redisson是一个在Redis基础上实现的Java驻内存数据网格框架,提供了丰富的分布式Java对象和服务。其分布式锁实现是企业级应用中的首选方案。

Redisson的主要特性

  • 完整的分布式锁实现(可重入锁、读写锁、公平锁等)

  • 内置看门狗机制,自动续期避免死锁

  • 支持多种部署模式(单机、集群、哨兵等)

  • 提供丰富的分布式数据结构

Redisson分布式锁架构

2.2 RLock接口与可重入锁

Redisson的分布式锁实现了Java标准库中的Lock接口,并扩展为RLock接口,提供分布式环境下的可重入锁功能。

RLock接口主要方法

  • lock():阻塞式获取锁

  • lockInterruptibly():可中断的锁获取

  • tryLock():尝试获取锁,立即返回结果

  • tryLock(long waitTime, long leaseTime, TimeUnit unit):带超时的锁获取

  • unlock():释放锁

  • forceUnlock():强制释放锁

可重入锁的实现原理

Redisson使用Redis的Hash结构存储锁信息,支持同一线程多次获取同一把锁。

锁存储结构

java 复制代码
键名: my_lock
Hash结构:
  字段: 客户端ID:线程ID
  值: 重入次数

示例代码

java 复制代码
// 获取锁实例
RLock lock = redissonClient.getLock("myLock");

try {
    // 获取锁(可重入)
    lock.lock();
    
    // 同一线程可以再次获取锁
    lock.lock();
    
    // 业务逻辑
    doBusiness();
    
} finally {
    // 需要释放两次
    lock.unlock();
    lock.unlock();
}

2.3 看门狗机制(续期与自动释放)

看门狗机制是Redisson分布式锁的核心特性,解决了锁过期时间设置的难题。

工作机制

  1. 客户端获取锁时,如果没有指定leaseTime(租约时间),Redisson会启动看门狗线程

  2. 看门狗线程每10秒检查一次客户端是否还持有锁

  3. 如果客户端仍持有锁,则将锁的过期时间重置为30秒

  4. 当锁被释放或客户端断开连接时,看门狗线程停止

2.4 公平锁、读写锁、联锁、红锁等

1. 公平锁(FairLock)

公平锁确保所有等待获取锁的线程按照请求顺序获得锁,避免线程饥饿现象。

Redisson的公平锁通过Redis的List和Sorted Set数据结构实现请求排队机制,多个数据结构协同工作:

  1. 等待队列(List):存储等待锁的线程请求ID

  2. 超时集合(Sorted Set):存储每个请求的超时时间

  3. 锁状态(Hash):存储当前锁的持有者信息

  4. 发布订阅频道:用于通知等待线程锁已释放

优势

  • 避免线程饥饿,所有请求都有机会获得锁

  • 按照请求顺序分配资源,更加公平

  • 适用于资源分配、任务调度等场景

劣势

  • 实现复杂,性能开销较大

  • 需要维护额外的队列数据结构

  • 相比非公平锁,吞吐量可能降低

2. 读写锁(ReadWriteLock)

读写锁允许多个读操作同时进行,但写操作是独占的。这种锁在读多写少的场景下能显著提高系统并发性能。

读写锁的实现原理

Redisson读写锁在Redis中使用不同的键来管理读锁和写锁:

  1. 读锁计数器:使用Redis的String结构记录当前持有读锁的线程数

  2. 写锁标记:使用Redis的String结构记录写锁持有者信息

  3. 互斥机制:读锁和写锁之间通过检查对方的状态实现互斥

3. 联锁(MultiLock)

联锁允许将多个独立的锁组合成一个锁,要求同时获取所有锁,要么全部获取成功,要么全部失败。适用于需要原子性操作多个资源的场景。

4. 红锁(RedLock)

红锁算法是基于多个独立Redis实例的分布式锁算法,旨在解决单点故障问题,提高锁的可用性。

RedLock算法的关键条件

  1. 实例独立性:每个Redis实例应该是独立部署的,避免单点故障

  2. 大多数原则:需要从大多数实例(N/2 + 1)获取锁成功

  3. 时钟同步:需要考虑各实例间的时钟差异

  4. 网络延迟:获取锁的总时间需要考虑网络延迟

2.5 各种锁的对比

锁类型 适用场景 并发性能 实现复杂度 容错能力 公平性
普通可重入锁 通用场景,单个资源互斥访问 中等 非公平
公平锁 需要按序访问的资源分配 较低 公平
读写锁 读多写少的共享资源访问 中等 可配置
联锁 需要原子操作多个资源 较低 非公平
红锁 高可用性要求的核心业务 很高 非公平

3. 看门狗机制(Watchdog)深度解析

3.1 为什么需要看门狗?

在分布式锁的使用中,锁过期时间的设置是一个难题:

  1. 设置过短:业务未完成,锁已过期,其他线程可能获取锁导致并发问题

  2. 设置过长:客户端崩溃后,锁长时间无法释放,影响系统可用性

  3. 难以预估:不同业务执行时间不同,难以统一设置合适的过期时间

看门狗机制通过自动续期解决了这个问题,让开发者无需关心锁过期时间。

3.2 看门狗的工作原理

看门狗机制的核心是后台定时任务,定期检查并续期锁的过期时间。

工作流程详细说明

关键配置参数

  • lockWatchdogTimeout:看门狗超时时间,默认30秒

  • 续期间隔:lockWatchdogTimeout / 3,默认10秒

3.3 源码分析

Redisson看门狗机制的核心代码位于RedissonLock类中:

1. 锁获取时启动看门狗

java 复制代码
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    // 尝试获取锁
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    
    if (ttl == null) {
        // 获取锁成功
        return;
    }
    
    // 如果leaseTime <= 0,启动看门狗
    if (leaseTime <= 0) {
        scheduleExpirationRenewal(threadId);
    }
}

2. 调度锁续期任务

java 复制代码
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(
        getEntryName(), entry);
    
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        // 启动续期任务
        renewExpiration();
    }
}

3. 续期任务实现

java 复制代码
private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    // 创建定时任务
    Timeout task = commandExecutor.getConnectionManager()
        .newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                if (ent == null) {
                    return;
                }
                
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }
                
                // 执行续期操作
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock expiration", e);
                        return;
                    }
                    
                    if (res) {
                        // 递归调用,继续下一次续期
                        renewExpiration();
                    }
                });
            }
        }, 
        internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 10秒后执行
    
    ee.setTimeout(task);
}

4. 续期Lua脚本

Lua 复制代码
-- KEYS[1]: 锁的key
-- ARGV[1]: 新的过期时间
-- ARGV[2]: 客户端标识(线程ID)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return 1; 
end; 
return 0;

4. 总结

Redis分布式锁是分布式系统中实现资源互斥访问的关键技术。从基础的SET命令实现到Redisson的高级特性,分布式锁的解决方案不断演进和完善。

Redisson分布式锁知识体系

核心要点总结

  1. 基础实现:SET NX PX + Lua脚本保证原子性,简单但功能有限

  2. Redisson优势:提供看门狗机制、可重入锁、多种锁类型等高级特性

  3. 看门狗机制:自动续期解决锁过期时间设置难题,提高系统可靠性

  4. 锁类型选择:根据业务场景选择合适的锁类型(普通锁、读写锁、公平锁、红锁等)

  5. 最佳实践:合理控制锁粒度、设置超时时间、完善异常处理、建立监控体系

相关推荐
独自破碎E2 小时前
说说Java中的反射机制
java·开发语言
一直都在5722 小时前
SpringBoot3 框架快速搭建与项目工程详解
java·开发语言
子云之风2 小时前
LSPosed 项目编译问题解决方案
java·开发语言·python·学习·android studio
小北方城市网2 小时前
SpringBoot 全局异常处理与接口规范实战:打造健壮可维护接口
java·spring boot·redis·后端·python·spring·缓存
独自破碎E2 小时前
什么是Spring IOC
java·spring·rpc
lendsomething2 小时前
graalvm使用实战:在java中执行js脚本
java·开发语言·javascript·graalvm
烤麻辣烫2 小时前
java进阶--刷题与详解-2
java·开发语言·学习·intellij-idea
期待のcode2 小时前
性能监控工具
java·开发语言·jvm
Chan162 小时前
【 微服务SpringCloud | 方案设计 】
java·spring boot·微服务·云原生·架构·intellij-idea