IT俱乐部 Redis Redission实现分布式锁lock()和tryLock()方法的区别小结

Redission实现分布式锁lock()和tryLock()方法的区别小结

本文主要介绍了 Redission实现分布式锁lock()和tryLock()方法的区别小结,具体如下:

 lock.lock(30, TimeUnit.SECONDS); // 尝试获取锁30秒,如果获取不到则放弃
//尝试获取锁,等待5秒,持有锁10秒钟
boolean success = lock.tryLock(5, 10, TimeUnit.SECONDS);

Redisson 是一种基于 Redis 的分布式锁框架,提供了 lock() 和 tryLock() 两种获取锁的方法。

lock() 方法是阻塞获取锁的方式,如果当前锁被其他线程持有,则当前线程会一直阻塞等待获取锁,直到获取到锁或者发生超时或中断等情况才会结束等待。该方法获取到锁之后可以保证线程对共享资源的访问是互斥的,适用于需要确保共享资源只能被一个线程访问的场景。Redisson 的 lock() 方法支持可重入锁和公平锁等特性,可以更好地满足多线程并发访问的需求。

而 tryLock() 方法是一种非阻塞获取锁的方式,在尝试获取锁时不会阻塞当前线程,而是立即返回获取锁的结果,如果获取成功则返回 true,否则返回 false。Redisson 的 tryLock() 方法支持加锁时间限制、等待时间限制以及可重入等特性,可以更好地控制获取锁的过程和等待时间,避免程序出现长时间无法响应等问题。

因此,两种获取锁的方式各有优缺点,在实际应用中需要根据具体场景和业务需求来选择合适的方法,以确保程序的正确性和高效性。

直接看代码例子lock.tryLock等待时间和持有时间都为0时。

    public String RedissonLock1() {
        RLock lock = redissonClient.getLock("order_lock");
        boolean success = true;
        try {
            System.out.println("获取锁前的时间:"+LocalDateTime.now());
            // 尝试获取锁,等待0秒,持有锁0秒钟
            success = lock.tryLock(0, 0, TimeUnit.SECONDS);
            // lock.lock(0, TimeUnit.SECONDS);
            System.out.println("获取锁后的时间:"+LocalDateTime.now());
            if (success) {
                System.out.println(Thread.currentThread().getName() + "获取到锁"+ LocalDateTime.now());
                // 模拟业务处理耗时
                // TimeUnit.SECONDS.sleep(3);
                // 模拟业务处理耗时 大于锁过期,可能导致非自己持有的锁被释放。
                TimeUnit.SECONDS.sleep(20);
            } else {
                System.out.println(Thread.currentThread().getName() + "未能获取到锁,已放弃尝试");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 判断当前线程是否持有锁
            if (success && lock.isHeldByCurrentThread()) {
                //释放当前锁
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁"+ LocalDateTime.now());
            }
        }
        return "";
    }

可以看到立即拿锁,拿不到立即就返回

 lock()方法的持有锁时间为0时

    public String RedissonLock2() {
        RLock lock = redissonClient.getLock("order_lock");
        boolean success = true;
        try {
            System.out.println("获取锁前的时间:"+LocalDateTime.now());
            // 尝试获取锁,等待5秒,持有锁10秒钟
           // success = lock.tryLock(0, 0, TimeUnit.SECONDS);
            lock.lock(0, TimeUnit.SECONDS);
            System.out.println("获取锁后的时间:"+LocalDateTime.now());
            // 模拟业务处理耗时
            // TimeUnit.SECONDS.sleep(3);
            // 模拟业务处理耗时 大于锁过期,可能导致非自己持有的锁被释放。
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 判断当前线程是否持有锁
            if (success && lock.isHeldByCurrentThread()) {
                //释放当前锁
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁"+ LocalDateTime.now());
            }
        }
        return "";
    }

 可以看到第二次获取锁时是阻塞的,等着业务处理了20s后,才获取到锁。

lock() 方法是阻塞获取锁的方式,而 tryLock() 方法是一种非阻塞获取锁的方式。

可以结合文章一起看 看懂Redisson分布式锁源码,其实并不难

看门狗续约时间默认是30秒

lock() 和 tryLock()都会用的 tryAcquireAsync(),看到区别,其实核心是leaseTime是否 > 0 

所以是否使用看门狗不算是tryLock和lock()的区别,可以看下面这5种情况。

            //走if (leaseTime > 0)逻辑
            lock.tryLock(5, 10, TimeUnit.SECONDS);
            lock.lock(10, TimeUnit.SECONDS);

            //不走if (leaseTime > 0)逻辑
            lock.tryLock(0, -1, TimeUnit.SECONDS);
            lock.lock(-1, TimeUnit.SECONDS);
            //lock()不带参数,默认leaseTime = -1
            lock.lock();

leaseTime不大于0时,会执行scheduleExpirationRenewal(threadId);

 当oldEntry == null时,执行renewExpiration(); 就是个定时器,即是看门狗核心逻辑了。

    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;
                }
                
                CompletionStage future = renewExpirationAsync(threadId);
                future.whenComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock {} expiration", getRawName(), e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }
                    
                    if (res) {
                        // reschedule itself
                        renewExpiration();
                    } else {
                        cancelExpirationRenewal(null);
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }

 其中有下面红圈这段逻辑,也就是看门狗之所以在三分之一的时候执行的原因, 没有自定义时

config.setLockWatchdogTimeout(10000); // 设置锁的看门狗超时时间为10秒

默认就是30秒 除以3  也就是10秒时执行。

 再看几段实现分布式锁核心的LUA脚本

1.拿锁LUA。

ChaGPT解释此段LUA:

这是 Redisson 中 RedissonLock 类实现的 tryLockInnerAsync() 方法。该方法用于尝试获取锁,它会先检查锁是否已经被其他线程持有,如果没有被持有,则将当前线程设置为锁的持有者,并返回 null;否则返回当前线程需要等待的时间,以便稍后再次尝试获取锁。

具体地说,Lua 脚本的逻辑是这样的:

  • 如果锁不存在或者锁已经被当前线程持有(使用 hash 存储锁的持有者),则将当前线程作为锁的持有者,并且设置锁的过期时间,并返回 null。这里的代码具体操作包括使用 HEXISTS 命令检查锁的持有情况,使用 HINCRBY 命令来设置持有者和持有计数器并提高计数器的值,以及使用 PEXPIRE 命令来设置锁的过期时间。
  • 如果锁已经被其他线程持有,则返回当前线程需要等待的时间,以便稍后再次尝试获取锁。这里使用 PTTL 命令来查询锁的过期时间,并返回给调用方。

需要注意的是,因为 tryLockInnerAsync 方法的实现是原子的,所以它可以保证在分布式环境下的正确性。同时,它也支持重入锁的功能,因为它保留了锁的持有者和持有计数器的值。

 2.看门狗续租LUA

ChaGPT解释此段LUA:

这是 Redisson 中 RedissonLock 类实现的 renewExpirationAsync(long threadId) 方法。该方法用于续期锁的过期时间,以避免锁在使用期间失效。首先,该方法会检查当前线程是否持有锁(通过 hash 存储锁的持有者),如果当前线程不是锁的持有者,则会直接返回 false 表示续期失败。如果当前线程是锁的持有者,则会将锁的过期时间重置为 internalLockLeaseTime 毫秒,并返回 true 表示续期成功。

具体来说,Lua 脚本的逻辑是这样的:

  • 如果当前线程不是锁的持有者,则直接返回 0(false)。
  • 如果当前线程是锁的持有者,则将锁的过期时间重置为 internalLockLeaseTime 毫秒,并返回 1(true)。

 3.解锁LUA

这是 Redisson 中 RedissonLock 类实现的 unlockInnerAsync(long threadId) 方法。该方法用于释放锁,首先它会检查当前线程是否持有锁(通过 hash 存储锁的持有者),如果当前线程不是锁的持有者,则直接返回 null;如果当前线程是锁的持有者,则将锁的计数器减一,如果减完之后计数器大于 0,则续期锁的过期时间,并返回 false 表示未成功释放锁;否则则删除该锁,并发布一个消息通知其他竞争者可以获取到该锁了,最后返回 true 表示成功释放锁。

具体来说,Lua 脚本的逻辑是这样的:

  • 如果当前线程不是锁的持有者,则直接返回 null。
  • 如果当前线程是锁的持有者,则使用 HINCRBY 命令将锁的计数器减一,并获取计数器的当前值。
  • 如果锁的计数器大于 0,则使用 PEXPIRE 命令续期锁的过期时间,并返回 0(false)。
  • 如果锁的计数器已经为 0,则使用 DEL 命令删除该锁,并使用 PUBLISH 命令发布一个消息通知其他竞争者可以获取到该锁了,最后返回 1(true)。

 4.预留了一个强制暴力解锁的方法给用户

//没有释放锁,导致整个系统的性能下降甚至故障,必须强行停止这个锁时 使用lock.forceUnlock();

这是 Redisson 中 RedissonLock 类实现的 forceUnlockAsync() 方法。该方法用于强制释放锁,取消锁定的状态。在方法内部,它会调用 cancelExpirationRenewal(null) 方法来停止看门狗线程的续期任务。然后使用 evalWriteAsync 方法执行 Lua 脚本,通过将锁的名字和订阅通道作为参数传入,删除锁并返回结果,同时发布一条消息到订阅通道上,表示这个锁被释放了。

具体地说,Lua 脚本的逻辑是这样的:

  • 使用 DEL 命令删除锁,如果返回值为 1,则说明锁被成功删除,否则说明当前线程并没有持有这个锁,直接返回 0。
  • 如果锁被成功删除,使用 PUBLISH 命令发布一条消息到订阅通道上,表示这个锁被释放了,并返回 1。

 在所有操作锁的地方使用LUA脚本,单线程,天然线程安全。

到此这篇关于Redission实现分布式锁lock()和tryLock()方法的区别小结的文章就介绍到这了,更多相关Redission lock()和tryLock() 内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!

本文收集自网络,不代表IT俱乐部立场,转载请注明出处。https://www.2it.club/database/redis/12952.html
上一篇
下一篇
联系我们

联系我们

在线咨询: QQ交谈

邮箱: 1120393934@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部