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

Redis分布式锁

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

1.1 使用SET命令实现

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

实现原理:

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

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

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

基本命令格式:

SET lock_key unique_value NX PX 30000

工作流程图:

基础实现代码示例:

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脚本保证原子性

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结构存储锁信息,支持同一线程多次获取同一把锁。

锁存储结构:

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

示例代码:



// 获取锁实例
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. 锁获取时启动看门狗:

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. 调度锁续期任务:

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. 续期任务实现:

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脚本:

-- 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. 最佳实践:合理控制锁粒度、设置超时时间、完善异常处理、建立监控体系

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐