【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取

简介: 本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。

引言

有加锁自然就有解锁,本篇则将围绕锁的释放锁Lua脚本进行深入剖析,另外,还将对阻塞和非阻塞两张方式分别如何获取锁进行比较。

可重入锁之释放锁

这里我们依然是按照步骤来看看释放锁是如何执行的。

1.首先从入口方法开始:

public void unlock() {
   
    try {
   
        get(unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException e) {
   
        if (e.getCause() instanceof IllegalMonitorStateException) {
   
            throw (IllegalMonitorStateException) e.getCause();
        } else {
   
            throw e;
        }
    }
}

// 异步解锁方法
public RFuture<Void> unlockAsync(long threadId) {
   
    // 调用解锁的 Lua 脚本
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    return future.thenAccept((opStatus) -> {
   
        // 解锁成功,取消看门狗续期
        cancelExpirationRenewal(threadId);

        // 如果解锁的不是自己的锁,抛出异常
        if (!opStatus) {
   
            throw new IllegalMonitorStateException(
                "attempt to unlock lock, not locked by current thread by node id: "
                + id + " thread-id: " + threadId);
        }
    });
}
AI 代码解读

2.核心解锁Lua脚本实现:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
   
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        // 检查锁是否存在
        "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
            "return nil;" +
        "end; " +

        // 计算当前线程的重入次数
        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +

        // 如果重入次数还大于0,则更新过期时间
        "if (counter > 0) then " +
            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "return 0; " +
        "end; " +

        // 重入次数为0,删除锁
        "redis.call('del', KEYS[1]); " +
        // 发布锁释放的消息
        "redis.call('publish', KEYS[2], ARGV[1]); " +
        "return 1; ",

        // 脚本参数
        Arrays.asList(
            getName(),                // KEYS[1] 锁名称
            getChannelName(),         // KEYS[2] 发布订阅的channel名称
            RedissonLockEntry.UNLOCK_MESSAGE,    // ARGV[1] 解锁消息
            internalLockLeaseTime,    // ARGV[2] 锁过期时间
            getLockName(threadId)     // ARGV[3] 线程标识
        ));
}
AI 代码解读

3.梳理流程

  • 首先进行解锁的前置检查:检查是否存在对应线程的锁,如果不存在,则返回nil。
  • 如果获取锁成功,则:处理重入计数,即将当前线程的重入计数减1;如果重入计数还大于0,表示还有重入,则重新设置过期时间,返回0则表示锁还未完全释放。
  • 完全释放锁,即:当计数器为0,删除整个锁并发布锁释放的消息,通知等待的线程,返回1则表示锁已完全释放。
  • 后续处理,需要:解锁成功后取消看门狗续期和处理异常情况。

可重入锁之阻塞和非阻塞获取锁

redisson提供了两种不同方式获取锁的封装,我们这里比较讲下:

1.非阻塞获取锁 (tryLock)

public boolean tryLock() {
   
    return tryLock(-1, -1, TimeUnit.MILLISECONDS);
}

// 带超时的非阻塞获取锁
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
   
    return tryLock(waitTime, -1, unit);
}

// 核心实现
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
   
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    long threadId = Thread.currentThread().getId();

    // 第一次尝试获取锁
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // ttl为空表示获取成功
    if (ttl == null) {
   
        return true;
    }

    // 如果没有等待时间,直接返回失败
    if (time <= 0) {
   
        return false;
    }

    // 计算剩余等待时间
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
   
        return false;
    }

    // 订阅锁释放通知
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
   
        return false;
    }

    try {
   
        while (true) {
   
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(leaseTime, unit, threadId);

            // 获取成功
            if (ttl == null) {
   
                return true;
            }

            // 超过等待时间,返回失败
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
   
                return false;
            }

            // 等待锁释放通知
            currentTime = System.currentTimeMillis();
            if (ttl >= 0 && ttl < time) {
   
                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
   
                getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
   
                return false;
            }
        }
    } finally {
   
        // 取消订阅
        unsubscribe(subscribeFuture, threadId);
    }
}
AI 代码解读

2.阻塞获取锁 (Lock)

public void lock() {
   
    try {
   
        lock(-1, null, false);
    } catch (InterruptedException e) {
   
        throw new IllegalStateException();
    }
}

// 带超时的阻塞获取锁
public void lock(long leaseTime, TimeUnit unit) {
   
    try {
   
        lock(leaseTime, unit, false);
    } catch (InterruptedException e) {
   
        throw new IllegalStateException();
    }
}

// 核心实现
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;
    }

    // 订阅锁释放通知
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    if (interruptibly) {
   
        subscribeFuture.syncUninterruptibly();
    } else {
   
        subscribeFuture.sync();
    }

    try {
   
        while (true) {
   
            ttl = tryAcquire(leaseTime, unit, threadId);
            // 获取成功
            if (ttl == null) {
   
                break;
            }

            // 等待锁释放通知
            if (ttl >= 0) {
   
                try {
   
                    getEntry(threadId).getLatch().await(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
   
                    if (interruptibly) {
   
                        throw e;
                    }
                    getEntry(threadId).getLatch().await();
                }
            } else {
   
                if (interruptibly) {
   
                    getEntry(threadId).getLatch().await();
                } else {
   
                    getEntry(threadId).getLatch().awaitUninterruptibly();
                }
            }
        }
    } finally {
   
        // 取消订阅
        unsubscribe(subscribeFuture, threadId);
    }
}
AI 代码解读

3.两种方式的关键区别:

  • 等待策略:

    tryLock:有限时间等待,超时返回false

    lock:无限等待直到获取到锁

  • 返回值:

    tryLock:返回boolean,表示是否获取成功

    lock:无返回值,要么获取成功,要么一直等待

  • 中断处理:

    tryLock:支持中断

    lock:默认不响应中断,但可以通过lockInterruptibly方法支持中断

小结

关于可重入锁的相关源码刨析就告一段落了,在接下来的篇章中我们将继续分析不同类型锁的实现。

目录
相关文章
|
12天前
|
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
62 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
21天前
|
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
本文详细解析了Redisson可重入锁的加锁流程。首先从`RLock.lock()`方法入手,通过获取当前线程ID并调用`tryAcquire`尝试加锁。若加锁失败,则订阅锁释放通知并循环重试。核心逻辑由Lua脚本实现:检查锁是否存在,若不存在则创建并设置重入次数为1;若存在且为当前线程持有,则重入次数+1。否则返回锁的剩余过期时间。此过程展示了Redisson高效、可靠的分布式锁机制。
43 0
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
486 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
|
1月前
|
Springboot使用Redis实现分布式锁
通过这些步骤和示例,您可以系统地了解如何在Spring Boot中使用Redis实现分布式锁,并在实际项目中应用。希望这些内容对您的学习和工作有所帮助。
181 83
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
分布式爬虫框架Scrapy-Redis实战指南
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁如何实现 ?
Redis分布式锁主要依靠一个SETNX指令实现的 , 这条命令的含义就是“SET if Not Exists”,即不存在的时候才会设置值。 只有在key不存在的情况下,将键key的值设置为value。如果key已经存在,则SETNX命令不做任何操作。 这个命令的返回值如下。 ● 命令在设置成功时返回1。 ● 命令在设置失败时返回0。 假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行S
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。
68 6
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
Redis,分布式缓存演化之路
本文介绍了基于Redis的分布式缓存演化,探讨了分布式锁和缓存一致性问题及其解决方案。首先分析了本地缓存和分布式缓存的区别与优劣,接着深入讲解了分布式远程缓存带来的并发、缓存失效(穿透、雪崩、击穿)等问题及应对策略。文章还详细描述了如何使用Redis实现分布式锁,确保高并发场景下的数据一致性和系统稳定性。最后,通过双写模式和失效模式讨论了缓存一致性问题,并提出了多种解决方案,如引入Canal中间件等。希望这些内容能为读者在设计分布式缓存系统时提供有价值的参考。感谢您的阅读!
135 6
Redis,分布式缓存演化之路
|
26天前
|
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
本文深入探讨了基于Redis实现分布式锁时遇到的细节问题及解决方案。首先,针对锁续期问题,提出了通过独立服务、获取锁进程自己续期和异步线程三种方式,并详细介绍了如何利用Lua脚本和守护线程实现自动续期。接着,解决了锁阻塞问题,引入了带超时时间的`tryLock`机制,确保在高并发场景下不会无限等待锁。最后,作为知识扩展,讲解了RedLock算法原理及其在实际业务中的局限性。文章强调,在并发量不高的场景中手写分布式锁可行,但推荐使用更成熟的Redisson框架来实现分布式锁,以保证系统的稳定性和可靠性。
48 0
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等