Redisson单机模式

article/2025/6/7 14:49:41

redisson调用unlock的过程

Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid)框架,提供了分布式和可扩展的数据结构和服务。Redisson 的 unlock 方法用于释放锁。下面是 unlock 方法的调用过程:

  1. 获取锁的状态:首先,Redisson 会检查当前线程是否持有锁。如果当前线程没有持有锁,则会抛出 IllegalMonitorStateException 异常。

  1. 尝试释放锁:如果当前线程持有锁,Redisson 会尝试释放锁。它会调用 Redis 的 eval 命令执行 Lua 脚本,以确保释放锁的操作是原子性的。Lua 脚本会检查锁的持有者是否是当前线程,如果是,则释放锁。

  1. 处理锁的计数:Redisson 支持可重入锁,因此在释放锁时需要处理锁的计数。如果当前线程持有的锁计数大于 1,则只减少计数而不真正释放锁;如果计数为 1,则真正释放锁。

  1. 通知等待的线程:如果锁被成功释放,Redisson 会通知等待的线程,以便它们可以尝试获取锁。

下面是一个简化的 unlock 方法的伪代码示例:

public void unlock() {// 获取当前线程的IDlong threadId = Thread.currentThread().getId();// 检查当前线程是否持有锁if (!isHeldByCurrentThread()) {throw new IllegalMonitorStateException("Current thread does not hold the lock");}// 尝试释放锁RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(),RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then " +"return nil; " +"end; " +"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +"if (counter > 0) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return 0; " +"else " +"redis.call('del', KEYS[1]); " +"return 1; " +"end; " +"return nil;",Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));// 处理释放锁的结果future.onComplete((res, e) -> {if (e != null) {// 处理异常} else if (res) {// 通知等待的线程signal();}});
}

这个伪代码展示了 unlock 方法的核心逻辑,包括检查锁的持有者、尝试释放锁、处理锁的计数以及通知等待的线程。实际的实现可能会更加复杂,包含更多的错误处理和边界情况处理。

在分布式环境中,Redisson 使用 Redis 的发布/订阅(Pub/Sub)机制来通知等待的线程。具体来说,当一个线程成功释放锁时,它会向一个特定的 Redis 频道发布消息,通知其他等待的线程锁已经被释放。其他等待的线程会订阅这个频道,并在收到消息后尝试获取锁

以下是一个简化的过程描述:

  1. 订阅频道当一个线程尝试获取锁但未成功时,它会订阅一个特定的 Redis 频道。这个频道通常与锁的名称相关联

  1. 发布消息:当持有锁的线程成功释放锁时,它会向这个频道发布一条消息,通知其他等待的线程锁已经被释放。

  1. 接收通知其他等待的线程会接收到这个消息,并尝试重新获取锁

下面是一个简化的伪代码示例,展示了如何使用 Redis 的发布/订阅机制来通知等待的线程:

public void unlock() {long threadId = Thread.currentThread().getId();if (!isHeldByCurrentThread()) {throw new IllegalMonitorStateException("Current thread does not hold the lock");}RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then " +"return nil; " +"end; " +"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +"if (counter > 0) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return 0; " +"else " +"redis.call('del', KEYS[1]); " +"redis.call('publish', KEYS[2], ARGV[3]); " + // 发布消息"return 1; " +"end; " +"return nil;",Arrays.<Object>asList(getName(), getChannelName()), internalLockLeaseTime, getLockName(threadId), "UNLOCKED");future.onComplete((res, e) -> {if (e != null) {// 处理异常} else if (res) {// 锁已成功释放}});
}public void lock() {long threadId = Thread.currentThread().getId();while (true) {RFuture<Boolean> future = tryAcquireLockAsync(threadId);future.onComplete((res, e) -> {if (e != null) {// 处理异常} else if (res) {// 成功获取锁return;}});// 订阅频道,等待通知RFuture<Void> subscribeFuture = commandExecutor.subscribe(getChannelName(), message -> {// 收到通知,尝试重新获取锁lock();});subscribeFuture.onComplete((res, e) -> {if (e != null) {// 处理异常}});// 等待一段时间后再尝试获取锁Thread.sleep(100);}
}

在这个伪代码示例中:

  • unlock 方法在成功释放锁后,通过 redis.call('publish', KEYS[2], ARGV[3]) 向频道发布消息。

  • lock 方法在尝试获取锁失败后,订阅频道并等待通知。当收到通知时,它会再次尝试获取锁。

通过这种方式,Redisson 可以在分布式环境中通知等待的线程锁已经被释放,从而实现分布式锁的协调。

Redis 的发布/订阅(Pub/Sub)是一种消息传递模式,发送者(发布者)将消息发送到频道,接收者(订阅者)订阅这些频道以接收消息。发布者和订阅者之间没有直接的联系,消息通过 Redis 服务器进行传递。

在Redisson中,锁的释放有两种方式:手动释放和自动超时释放。无论是哪种方式,都会涉及到发布事件到频道的机制。

  1. 手动释放锁:当你调用 unlock 方法手动释放锁时,Redisson 会发布一个解锁事件到相应的频道。其他订阅了该频道的线程会收到这个事件,并尝试获取锁。

  1. 锁超时释放当锁的持有时间超过了设定的超时时间,Redisson 会自动释放锁。此时,Redisson 同样会发布一个解锁事件到相应的频道。其他订阅了该频道的线程会收到这个事件,并尝试获取锁

因此,无论是手动释放锁还是锁超时释放,Redisson 都会发布解锁事件到频道,通知其他线程锁已经被释放。其他线程通过订阅该频道,接收到解锁事件后,会尝试重新获取锁。

总结:

  • 手动释放锁:调用 unlock 方法,发布解锁事件到频道。

  • 锁超时释放:锁超时自动释放,发布解锁事件到频道。

在这两种情况下,其他线程都能通过订阅频道接收到解锁事件,并尝试获取锁。

在Redisson中,锁的获取可以是公平锁也可以是非公平锁,具体取决于你使用的锁类型。

  1. 非公平锁(默认):Redisson 默认提供的锁是非公平锁。非公平锁的特点是,锁的获取顺序不一定按照线程请求的顺序来,任何线程都有可能在锁释放时立即获取到锁。

  1. 公平锁:Redisson 也提供了公平锁。公平锁的特点是,锁的获取顺序是按照线程请求的顺序来进行的,先请求锁的线程会先获取到锁。Redisson 的公平锁是通过 RLock 接口的 fairLock 方法来实现的。

以下是如何使用这两种锁的示例:

  • 非公平锁(默认):

     
    RLock lock = redisson.getLock("myLock");
    lock.lock();
    try {// 业务逻辑
    } finally {lock.unlock();
    }

    • 公平锁

       
      RLock fairLock = redisson.getFairLock("myFairLock");
      fairLock.lock();
      try {// 业务逻辑
      } finally {fairLock.unlock();
      }

      总结:

      • 非公平锁:默认情况下,Redisson 提供的是非公平锁。

      • 公平锁:可以通过 getFairLock 方法获取公平锁,确保锁的获取顺序按照线程请求的顺序进行。

      RLock 是 Redisson 提供的分布式锁接口,它是基于 Redis 实现的。RLock 提供了类似于 Java 标准库中的 ReentrantLock 的功能,但它是分布式的,可以在多个 JVM 实例之间共享。

      RLockReentrantLock 的关系

      1. 相似点

        1. 接口和方法RLock 提供的接口和方法与 ReentrantLock 类似,包括 lock(), unlock(), tryLock(), lockInterruptibly() 等方法。

        2. 可重入性RLockReentrantLock 都是可重入锁,这意味着同一个线程可以多次获取同一把锁,而不会发生死锁。

      1. 不同点

        1. 分布式特性ReentrantLock 是本地锁,只能在单个 JVM 实例中使用。而 RLock 是分布式锁,可以在多个 JVM 实例之间共享,适用于分布式系统。

        2. 实现机制ReentrantLock 是基于 Java 内部的同步机制实现的,而 RLock 是基于 Redis 实现的,通过 Redis 的原子操作来保证分布式锁的正确性。

      使用示例

      • ReentrantLock** 示例**:

         
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {// 业务逻辑
        } finally {lock.unlock();
        }

        • RLock** 示例**:

           
          RLock lock = redisson.getLock("myLock");
          lock.lock();
          try {// 业务逻辑
          } finally {lock.unlock();
          }

          RLock 的其他功能

          除了基本的锁功能,RLock 还提供了一些高级功能:

          • 公平锁:通过 getFairLock 方法获取公平锁。

          • 读写锁:通过 getReadWriteLock 方法获取读写锁。

          • 联锁:通过 getMultiLock 方法获取联锁,可以同时锁定多个 RLock 实例。

          • 红锁:通过 getRedLock 方法获取红锁,适用于 Redis 集群环境。

          总结

          • ReentrantLock:本地锁,只能在单个 JVM 实例中使用。

          • RLock:分布式锁,可以在多个 JVM 实例之间共享,适用于分布式系统。提供了类似 ReentrantLock 的接口和方法,但基于 Redis 实现。

          发布/订阅的基本操作

          1. 订阅频道:使用 SUBSCRIBE 命令订阅一个或多个频道。

          2. 发布消息:使用 PUBLISH 命令向一个频道发布消息。

          3. 接收消息:订阅者会自动接收并处理发布到它所订阅频道的消息。

          示例

          以下是一个简单的示例,展示了如何使用 Redis 的发布/订阅功能。

          订阅频道

          假设我们有一个 Redis 客户端订阅了频道 my_channel

          import redis.clients.jedis.Jedis;
          import redis.clients.jedis.JedisPubSub;public class Subscriber {public static void main(String[] args) {Jedis jedis = new Jedis("localhost");jedis.subscribe(new JedisPubSub() {@Overridepublic void onMessage(String channel, String message) {System.out.println("Received message: " + message + " from channel: " + channel);}}, "my_channel");}
          }

          在这个示例中,JedisPubSub 类的 onMessage 方法会在接收到消息时被调用。

          发布消息

          另一个 Redis 客户端可以向 my_channel 发布消息:

          import redis.clients.jedis.Jedis;public class Publisher {public static void main(String[] args) {Jedis jedis = new Jedis("localhost");jedis.publish("my_channel", "Hello, Redis!");}
          }

          当发布者发布消息后,订阅者会接收到并处理该消息。

          使用场景

          Redis 的发布/订阅功能适用于以下场景:

          1. 实时消息传递:如聊天室、实时通知等。

          2. 事件驱动架构:如微服务之间的事件通知。

          3. 数据更新通知:如缓存失效通知。

          注意事项

          1. 消息丢失:如果订阅者在消息发布时未连接到 Redis 服务器,则会错过该消息。

          2. 消息顺序:Redis 保证消息按发布顺序传递给订阅者。

          3. 性能:发布/订阅模式适用于低延迟、高吞吐量的场景,但在大量消息和订阅者的情况下,可能会影响 Redis 的性能。

          通过 Redis 的发布/订阅功能,可以实现简单而高效的消息传递机制,适用于多种实时通信场景。

          Redis Lua 是指在 Redis 中使用 Lua 脚本进行编程。Redis 是一个高性能的键值存储数据库,而 Lua 是一种轻量级的脚本语言。通过在 Redis 中使用 Lua 脚本,可以实现一些复杂的操作和逻辑,从而提高性能和灵活性。

          使用 Redis Lua 脚本的主要优点包括:

          1. 原子性:Lua 脚本在 Redis 中是原子执行的,这意味着在脚本执行期间不会有其他命令插入,从而保证了操作的原子性。

          2. 性能:将多个 Redis 命令组合成一个 Lua 脚本,可以减少客户端与服务器之间的通信开销,从而提高性能。

          3. 灵活性:Lua 脚本可以实现一些复杂的逻辑和操作,这些操作可能无法通过单个 Redis 命令实现。

          在 Redis 中使用 Lua 脚本的基本步骤如下:

          1. 编写 Lua 脚本。

          2. 使用 EVAL 命令将 Lua 脚本发送到 Redis 服务器执行。

          例如,以下是一个简单的 Lua 脚本示例,该脚本将两个键的值相加并返回结果:

          local value1 = redis.call('GET', KEYS[1])
          local value2 = redis.call('GET', KEYS[2])
          return tonumber(value1) + tonumber(value2)

          可以使用以下命令在 Redis 中执行该脚本:

          EVAL "local value1 = redis.call('GET', KEYS[1]); local value2 = redis.call('GET', KEYS[2]); return tonumber(value1) + tonumber(value2)" 2 key1 key2

          在这个命令中,2 表示有两个键作为参数,key1key2 是传递给脚本的键名。

          Redis 的 NIO(Non-blocking I/O,非阻塞 I/O)是指 Redis 使用了一种非阻塞的网络 I/O 模型来处理客户端请求。非阻塞 I/O 模型使得 Redis 能够高效地处理大量并发连接,从而提高性能和吞吐量。

          在传统的阻塞 I/O 模型中,每个客户端连接都会占用一个线程,当线程等待 I/O 操作完成时(例如读取数据或写入数据),该线程会被阻塞,无法执行其他任务。这种模型在处理大量并发连接时会导致线程资源耗尽,性能下降。

          而在非阻塞 I/O 模型中,I/O 操作不会阻塞线程。当一个 I/O 操作无法立即完成时,线程可以继续执行其他任务,而不是等待 I/O 操作完成。Redis 使用了基于事件驱动的非阻塞 I/O 模型,通过单线程事件循环来处理所有客户端连接和请求。

          Redis 的非阻塞 I/O 模型的实现主要依赖于以下几个关键组件:

          1. 事件循环:Redis 使用一个单线程事件循环来处理所有的 I/O 事件。事件循环会不断地检查是否有新的 I/O 事件(例如新的客户端连接、可读或可写的套接字等),并调用相应的事件处理器来处理这些事件。

          1. 多路复用:Redis 使用多路复用技术(如 selectpollepoll 等)来同时监视多个文件描述符(套接字)。多路复用允许 Redis 在一个线程中高效地处理多个客户端连接。

          1. 事件处理器:Redis 为不同类型的 I/O 事件(如读事件、写事件、连接事件等)注册了相应的事件处理器。当事件循环检测到某个事件发生时,会调用相应的事件处理器来处理该事件。

          通过使用非阻塞 I/O 模型,Redis 能够在单线程中高效地处理大量并发连接,从而实现高性能和高吞吐量。这也是 Redis 能够在实际应用中表现出色的一个重要原因。

          在Redis中,selectpollepoll 是三种不同的I/O多路复用机制,用于处理多个网络连接。它们的主要作用是提高Redis在高并发场景下的性能。下面是对这三种机制的简要介绍:

          1. select:

            1. select 是最早的I/O多路复用机制之一。

            2. 它使用一个固定大小的数组来存储文件描述符(file descriptors)。

            3. 每次调用 select 时,内核会遍历整个数组,检查每个文件描述符的状态。

            4. 由于需要遍历整个数组,select 的性能在文件描述符数量较多时会显著下降。

            5. select 的文件描述符数量受限于 FD_SETSIZE,通常是1024。

          1. poll:

            1. pollselect 的改进版本。

            2. 它使用一个链表来存储文件描述符,因此没有 select 的文件描述符数量限制。

            3. 每次调用 poll 时,内核仍然需要遍历整个链表,检查每个文件描述符的状态。

            4. 虽然 poll 在文件描述符数量上没有限制,但在高并发场景下,性能仍然会受到影响。

          1. epoll:

            1. epoll 是Linux特有的I/O多路复用机制,专为高并发场景设计。

            2. 它使用一个事件驱动的机制,只在文件描述符状态发生变化时通知应用程序。

            3. epoll 使用一个红黑树来管理文件描述符,并通过一个双向链表来存储就绪的文件描述符。

            4. 由于 epoll 只在状态变化时通知,因此在高并发场景下性能非常高。

            5. epoll 没有文件描述符数量的限制,适用于大规模并发连接。

          Redis默认使用 epoll(在Linux系统上),因为它在高并发场景下性能最佳。如果 epoll 不可用,Redis 会退而使用 pollselect

          通俗来说,**Redis 的 epoll** 就像是一个“高效的门卫”,专门用来管理成千上万个客户端的网络请求。举个例子:

          假设你开了一家餐厅,只有一个服务员(单线程的 Redis)。传统方式是服务员挨个问每桌客人:“要点菜吗?”(类似轮询),效率很低。而 epoll 的作用是:

          1. 不用挨个问:服务员(Redis)只需要坐在门口,拿一个“监控屏幕”(epoll 实例)。

          2. 谁准备好就通知谁:当某桌客人主动举手(数据到达)时,屏幕会立刻提示服务员去处理这一桌。

          3. 省时省力:服务员不用浪费时间在“空等”或“反复询问”上,可以专注处理真正需要服务的客人。

          为什么 Redis 要用 epoll?

          • 单线程也能高并发:Redis 核心逻辑是单线程的,但通过 **epoll 多路复用技术**,它能同时监控大量网络连接,只处理“有数据到达”的请求,避免阻塞。

          • 性能优势:相比传统的 select/poll,epoll 使用红黑树管理连接,事件触发更高效,尤其适合连接数多但活跃请求少的场景(比如 Redis 的高并发读取)。

          举个实际场景:

          当 1 万个客户端同时连接 Redis 时,epoll 会快速识别哪些客户端发送了命令(比如读取缓存),并通知 Redis 依次处理这些命令,而不会让其他客户端“干等着”。这就是 Redis 单线程却能支持高吞吐量 的关键设计之一。

          简单总结:**epoll 是 Redis 高效处理海量网络请求的“智能调度器”**,让它用最少的资源,快速响应最多的客户端。

          Redis 是线程安全的。Redis 是一个单线程的服务器,这意味着它在任何给定时间只会处理一个命令。通过这种方式,Redis 避免了多线程编程中常见的竞争条件和死锁问题,从而保证了线程安全性。

          不过,虽然 Redis 本身是单线程的,但它的客户端库通常是多线程的。因此,在使用 Redis 客户端时,仍然需要注意线程安全问题,确保在多线程环境中正确使用客户端库。

          在 Redis 中,实现原子操作并不一定必须使用 Lua 脚本。Redis 提供了多种方式来实现原子操作,以下是几种常见的方法:

          1. 单个命令的原子性

          1. Redis 的大多数单个命令都是原子操作。例如,INCRDECRLPUSHRPOP 等命令都是原子操作。

          1. 事务(Transactions)

          1. Redis 提供了事务功能,可以使用 MULTIEXECDISCARDWATCH 命令来实现一组命令的原子执行。事务中的命令会按顺序执行,直到执行 EXEC 命令时,所有命令会被一次性执行。

          MULTI
          INCR key1
          INCR key2
          EXEC

          1. 乐观锁(Optimistic Locking)

          1. 使用 WATCH 命令可以实现乐观锁。WATCH 命令会监视一个或多个键,在执行 EXEC 命令之前,如果任何一个被监视的键发生了变化,事务会被中止。

          WATCH key1 key2
          MULTI
          INCR key1
          INCR key2
          EXEC

          1. Lua 脚本

          1. Lua 脚本可以将多个操作封装在一个脚本中,保证这些操作的原子性。Redis 会在执行 Lua 脚本时锁住所有涉及的键,确保脚本中的所有命令作为一个原子操作执行。

          local value1 = redis.call('INCR', 'key1')
          local value2 = redis.call('INCR', 'key2')
          return {value1, value2}

          综上所述,虽然 Lua 脚本是实现复杂原子操作的强大工具,但在 Redis 中实现原子操作并不一定必须使用 Lua 脚本。根据具体需求,可以选择合适的方法来实现原子操作。

          Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid)框架,它提供了许多分布式的数据结构和服务。Redisson 的看门狗机制(Watchdog)是其分布式锁实现中的一个重要特性,用于自动续期锁的过期时间,防止锁在持有期间意外过期

          在分布式系统中,分布式锁通常是通过设置一个带有过期时间的键来实现的。这样可以防止在持有锁的客户端崩溃或网络分区的情况下,锁永远不会被释放。然而,如果锁的持有时间超过了预设的过期时间,锁可能会被意外释放,从而导致并发问题。

          Redisson 的看门狗机制通过以下方式解决了这个问题:

          1. 自动续期:当一个客户端成功获取到锁后,Redisson 会启动一个看门狗线程,该线程会定期检查锁的状态,并在锁即将过期时自动续期锁的过期时间。这样可以确保锁在持有期间不会意外过期。

          1. 可配置的过期时间:看门狗机制的默认锁过期时间是 30 秒,但可以通过配置进行调整。用户可以根据具体需求设置合适的过期时间。

          1. 锁的释放:当客户端显式释放锁时,看门狗线程会停止续期操作,锁会被正常释放。

          以下是一个使用 Redisson 分布式锁的示例代码:

          import org.redisson.Redisson;
          import org.redisson.api.RLock;
          import org.redisson.api.RedissonClient;
          import org.redisson.config.Config;public class RedissonLockExample {public static void main(String[] args) {// 创建配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");// 创建 Redisson 客户端RedissonClient redisson = Redisson.create(config);// 获取分布式锁RLock lock = redisson.getLock("myLock");// 尝试获取锁lock.lock();try {// 执行需要加锁的操作System.out.println("Lock acquired, executing protected code...");// 模拟长时间操作Thread.sleep(60000);} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放锁lock.unlock();System.out.println("Lock released.");}// 关闭 Redisson 客户端redisson.shutdown();}
          }

          在这个示例中,lock.lock() 方法会启动看门狗线程,确保锁在持有期间不会过期。即使 Thread.sleep(60000) 模拟的操作时间超过了默认的锁过期时间(30 秒),看门狗机制也会自动续期锁的过期时间,确保锁不会被意外释放。

          Redisson 是一个在 Redis 上实现的分布式 Java 数据结构和服务的客户端。Redisson 提供了许多高级功能,其中之一就是看门狗机制(Watchdog Mechanism)。

          看门狗机制主要用于分布式锁的自动续期。具体来说,当一个线程获取到锁之后,如果在锁的持有期间内没有主动释放锁,看门狗机制会自动延长锁的有效期,防止锁过期被其他线程获取。

          看门狗机制的工作原理

          1. 获取锁:当一个线程获取到锁时,Redisson 会默认设置一个锁的有效期(例如 30 秒)。

          2. 启动看门狗:获取锁后,Redisson 会启动一个看门狗定时任务,这个任务会在锁的有效期到期之前(例如 10 秒前)自动延长锁的有效期。

          3. 自动续期:看门狗定时任务会不断地延长锁的有效期,直到线程主动释放锁或者程序结束。

          代码示例

          以下是一个使用 Redisson 分布式锁的简单示例:

          import org.redisson.Redisson;
          import org.redisson.api.RLock;
          import org.redisson.api.RedissonClient;
          import org.redisson.config.Config;public class RedissonWatchdogExample {public static void main(String[] args) {// 配置 RedissonConfig config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);// 获取锁RLock lock = redisson.getLock("myLock");try {// 尝试加锁lock.lock();// 执行业务逻辑System.out.println("Lock acquired, executing business logic...");// 模拟长时间的业务处理Thread.sleep(60000); // 60 秒} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放锁lock.unlock();System.out.println("Lock released.");}// 关闭 Redisson 客户端redisson.shutdown();}
          }

          在这个示例中,线程获取到锁后会执行一个长时间的业务逻辑(模拟 60 秒)。由于看门狗机制的存在,锁会在业务逻辑执行期间不断续期,防止锁过期。

          配置看门狗

          Redisson 的看门狗机制默认是开启的,并且默认的锁续期时间是 30 秒。如果需要自定义看门狗的行为,可以在配置中进行调整:

          config.setLockWatchdogTimeout(45000); // 设置看门狗超时时间为 45 秒

          通过这种方式,可以根据具体的业务需求调整看门狗的超时时间。

          总结

          Redisson 的看门狗机制通过自动续期锁的有效期,确保了分布式锁在长时间业务处理中的可靠性,避免了锁过期导致的并发问题。

          Redisson的看门狗(WatchDog)机制通过以下规则决定是否续期:

          1. 触发条件当使用lock()方法获取锁且**未显式指定锁超时时间(leaseTime)**时,看门狗会自动生效。若手动设置leaseTime,则需自行处理续期逻辑。

          1. 续期判断逻辑

            1. 看门狗默认每 10秒(超时时间的1/3) 检查一次锁状态。

            2. 通过Lua脚本验证当前线程是否仍持有锁,若持有则**重置锁超时时间为30秒**(默认值,可通过lockWatchdogTimeout配置修改)。

            3. 若锁已被释放或业务完成,看门狗会停止续期(通过移除EXPIRATION_RENEWAL_MAP中的记录)。

          1. 终止续期的场景

            1. 业务代码执行完毕,显式调用unlock()释放锁。

            2. 持有锁的线程异常退出或进程终止,看门狗线程随之停止,锁超时后自动释放。

          核心设计目标:在避免死锁的同时,确保业务执行期间锁的持有权,防止因超时导致并发问题。

          如果业务完成,但是忘记手动释放锁,看门狗会续期吗

          如果业务代码执行完成但**忘记手动释放锁(unlock())**,看门狗会**持续为锁续期**,直到以下条件满足之一:

          1. 进程或线程终止:若持有锁的线程或进程崩溃、被终止(如服务器宕机),看门狗线程随之停止,锁最终会在**默认30秒超时**后自动释放。

          2. 锁被其他方式强制释放:例如管理员手动介入删除锁,或Redis触发了数据过期策略。


          潜在风险与建议:

          • 风险忘记释放锁 + 看门狗续期会导致锁长期占用,可能引发**死锁假象**(其他线程无法获取锁)。

          • 解决方案

            • 使用 tryLock()lock() 时,**务必在finally块中调用****unlock()**(即使业务异常)。

            • 若需避免手动释放,可设置锁超时时间(leaseTime参数),但需确保业务在超时前完成。

            • 监控锁持有时间,通过RedissonClient.getLock("lockName").remainTimeToLive()检查锁剩余存活时间。

          核心结论看门狗仅依赖锁持有状态续期,**不感知业务逻辑是否完成**。**开发者必须主动释放锁**,否则锁会因续期机制长期存在。

          <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
          }

          这段代码是一个用于尝试获取分布式锁的异步方法。它使用了Redis作为分布式锁的存储介质。下面是对这段代码的详细解释:

          方法签名

          <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command)
          • <T>:泛型类型参数,表示返回值的类型。

          • RFuture<T>:表示一个异步操作的结果,类似于Java的Future,但用于异步操作。

          • tryLockInnerAsync:方法名称,表示尝试异步获取锁。

          • 参数:

            • waitTime:等待时间。

            • leaseTime:租约时间,即锁的有效期。

            • unit:时间单位,表示waitTimeleaseTime的单位。

            • threadId:线程ID,用于标识请求锁的线程。

            • command:Redis命令,表示要执行的具体Redis操作。

          方法体

          return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)}
          );
          • this.evalWriteAsync(...):调用异步执行Redis写操作的方法。

            • this.getRawName():获取锁的名称。

            • LongCodec.INSTANCE:用于编码和解码数据的编解码器。

            • command:传入的Redis命令。

            • Lua脚本:

              if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; 
              end; 
              return redis.call('pttl', KEYS[1]);
              • if ((redis.call('exists', KEYS[1]) == 0) or (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:返回nil,表示成功获取锁。

              • end:结束if语句。

              • return redis.call('pttl', KEYS[1]):返回键的剩余生存时间(TTL)。

            • Collections.singletonList(this.getRawName()):将锁的名称作为键传入Lua脚本。

            • new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)}:将租约时间(毫秒)和锁的名称(包含线程ID)作为参数传入Lua脚本。

          总结

          这段代码的主要功能是尝试获取一个分布式锁。如果锁不存在或者当前线程已经持有锁,则增加锁的计数并设置锁的过期时间;否则,返回锁的剩余生存时间。通过异步方式执行,可以避免阻塞线程,提高性能。

          在Redis中使用Lua脚本时,可以通过redis.callredis.pcall来执行Redis命令,并且可以通过KEYSARGV来传递参数。以下是详细的说明和示例:

          传递参数的方式

          • KEYS:用于传递键名。KEYS是一个数组,包含所有传递的键名。

          • ARGV:用于传递其他参数。ARGV是一个数组,包含所有传递的非键名参数。

          示例

          假设我们有一个Lua脚本,需要传递两个键名和两个参数:

          local key1 = KEYS[1]
          local key2 = KEYS[2]
          local arg1 = ARGV[1]
          local arg2 = ARGV[2]-- 示例操作:将arg1的值设置到key1,将arg2的值设置到key2
          redis.call('set', key1, arg1)
          redis.call('set', key2, arg2)-- 返回结果
          return {key1, key2, arg1, arg2}

          在Java中调用Lua脚本

          假设我们使用Jedis库来调用上述Lua脚本:

          import redis.clients.jedis.Jedis;public class RedisLuaExample {public static void main(String[] args) {// 连接到Redis服务器Jedis jedis = new Jedis("localhost", 6379);// Lua脚本String luaScript = "local key1 = KEYS[1] " +"local key2 = KEYS[2] " +"local arg1 = ARGV[1] " +"local arg2 = ARGV[2] " +"redis.call('set', key1, arg1) " +"redis.call('set', key2, arg2) " +"return {key1, key2, arg1, arg2}";// 定义键名和参数List<String> keys = Arrays.asList("key1", "key2");List<String> args = Arrays.asList("value1", "value2");// 执行Lua脚本Object result = jedis.eval(luaScript, keys, args);// 打印结果System.out.println(result);}
          }

          解释

          1. Lua脚本

            1. local key1 = KEYS[1]:获取第一个键名。

            2. local key2 = KEYS[2]:获取第二个键名。

            3. local arg1 = ARGV[1]:获取第一个参数。

            4. local arg2 = ARGV[2]:获取第二个参数。

            5. redis.call('set', key1, arg1):将第一个参数的值设置到第一个键。

            6. redis.call('set', key2, arg2):将第二个参数的值设置到第二个键。

            7. return {key1, key2, arg1, arg2}:返回键名和参数。

          1. Java代码

            1. 连接到Redis服务器。

            2. 定义Lua脚本。

            3. 定义键名和参数。

            4. 使用jedis.eval方法执行Lua脚本,传入脚本、键名列表和参数列表。

            5. 打印结果。

          通过这种方式,可以在Lua脚本中使用传入的键名和参数,执行相应的Redis命令。

          Lua脚本存在的原因主要有以下几个方面:

          1. 轻量级和高效:Lua是一种轻量级的脚本语言,设计之初就考虑了嵌入式系统的需求。它的解释器非常小巧,运行速度快,适合在资源受限的环境中使用。

          1. 嵌入性强:Lua可以很容易地嵌入到其他编程语言(如C、C++)中,作为一种嵌入式脚本语言使用。这使得开发者可以在主程序中调用Lua脚本,实现灵活的扩展和动态配置。

          1. 易于学习和使用:Lua的语法简单明了,学习曲线平缓,开发者可以快速上手并编写脚本。

          1. 跨平台:Lua是跨平台的,可以在多种操作系统和硬件平台上运行,这使得它在游戏开发、嵌入式系统、网络应用等领域得到了广泛应用。

          1. 强大的扩展性:Lua提供了丰富的标准库和强大的扩展机制,开发者可以根据需要扩展Lua的功能,满足各种应用场景的需求。

          1. 社区支持:Lua有一个活跃的社区,提供了大量的文档、教程和第三方库,帮助开发者解决问题并提高开发效率。

          综上所述,Lua脚本的存在为开发者提供了一种高效、灵活、易用的解决方案,广泛应用于游戏开发、嵌入式系统、网络应用等领域。

          Redis 的发布/订阅(Pub/Sub)和 Kafka 都是消息传递系统,但它们在设计目标、架构和使用场景上有显著的区别。以下是它们的主要区别:

          1. 设计目标和使用场景

          • Redis Pub/Sub

            • 设计目标:Redis 的 Pub/Sub 机制主要用于实时消息传递,适用于需要低延迟、实时性高的场景。

            • 使用场景:适用于实时聊天、通知系统、实时数据流等需要快速响应的场景。

            • 消息持久化:Redis Pub/Sub 不提供消息持久化功能,消息一旦发布,如果没有订阅者接收,消息就会丢失。

          • Kafka

            • 设计目标:Kafka 是一个分布式流处理平台,设计用于高吞吐量、持久化和分布式的消息传递。

            • 使用场景:适用于日志收集、事件追踪、数据流处理、消息队列等需要高吞吐量和持久化的场景。

            • 消息持久化:Kafka 提供消息持久化功能,消息会被存储在磁盘上,消费者可以在任何时候读取消息。

          2. 架构

          • Redis Pub/Sub

            • 架构:Redis 的 Pub/Sub 是基于内存的,消息发布者将消息发布到频道,所有订阅该频道的订阅者都会接收到消息。

            • 消息传递:消息是实时传递的,没有持久化,订阅者必须在线才能接收到消息。

          • Kafka

            • 架构:Kafka 是一个分布式系统,由多个 Broker 组成,消息被发布到主题(Topic),每个主题可以有多个分区(Partition)。

            • 消息传递:消息被持久化到磁盘,消费者可以在任何时候读取消息,支持消息的回溯和重放。

          3. 消息模型

          • Redis Pub/Sub

            • 消息模型:发布/订阅模型,发布者将消息发布到频道,所有订阅该频道的订阅者都会接收到消息。

            • 消息顺序:消息是按发布顺序传递的,但没有持久化,无法保证消息的可靠性。

          • Kafka

            • 消息模型:发布/订阅模型,消息被发布到主题,消费者从主题的分区中读取消息,同一个groupId只有一个消费者获取到消息。

            • 消息顺序:在同一个分区内,消息是按发布顺序存储和传递的,Kafka 提供了消息的持久化和可靠性保证。

          4. 性能和扩展性

          • Redis Pub/Sub

            • 性能:Redis Pub/Sub 的性能非常高,适用于低延迟的实时消息传递。

            • 扩展性:Redis 是单线程的,扩展性有限,适用于中小规模的实时消息传递。

          • Kafka

            • 性能:Kafka 设计用于高吞吐量的消息传递,能够处理大量的消息。

            • 扩展性:Kafka 是分布式系统,具有很好的扩展性,可以通过增加 Broker 来扩展系统的处理能力。

          总结

          • Redis Pub/Sub:适用于低延迟、实时性高的场景,不提供消息持久化,适用于中小规模的实时消息传递。

          • Kafka:适用于高吞吐量、持久化和分布式的消息传递,适用于日志收集、事件追踪、数据流处理等需要高可靠性和扩展性的场景。

          Redisson 作为 Redis 的 Java 客户端,在分布式场景中表现突出,以下是其核心优缺点分析:


          优点

          1. 分布式特性丰富提供分布式锁、集合、队列等高级数据结构,支持可重入锁、公平锁、联锁等机制,并解决**锁续期(看门狗机制)**和主从一致性等问题。

          2. 线程安全与高可用基于 Netty 实现异步非阻塞通信,支持多线程安全操作,且天然适配 Redis 集群和哨兵模式。

          3. 与 Spring 生态无缝集成提供 Spring Cache、Session 管理等模块化支持,简化分布式场景下的开发。

          4. 复杂场景优化支持数据序列化、自动重试、限流策略,并兼容多种编码协议(如 JSON、Avro)。


          缺点

          1. 学习成本较高抽象层级较高,API 设计复杂,需熟悉分布式概念(如锁的续期、RedLock 算法)。

          2. 部分 Redis 特性不支持对字符串操作、排序、事务、管道等原生 Redis 功能的支持较弱。

          3. 性能开销依赖 Netty 框架和复杂逻辑(如锁续期),在简单场景下可能不如轻量级客户端(如 Jedis)高效。

          4. 依赖稳定性风险强依赖 Redis 可用性,若 Redis 集群故障,分布式锁等机制可能失效。


          维度

          优势

          局限

          功能特性

          支持分布式锁、集合、队列等高级功能,解决锁续期、主从一致性

          不支持原生 Redis 的字符串操作、事务、管道等特性

          开发体验

          与 Spring 深度集成,提供线程安全和易用的 API

          API 抽象度高,学习曲线陡峭

          性能与扩展性

          基于 Netty 实现高并发,适配集群/哨兵模式

          复杂逻辑(如锁续期)可能增加延迟,轻量级场景性能不如 Jedis

          适用场景

          适合分布式系统(如微服务锁控制、分布式集合操作)

          简单 KV 存储或高频字符串操作场景建议选择其他客户端(如 Lettuce/Jedis)


          总结:Redisson 是构建分布式系统的利器,尤其在需要**复杂锁控制**或**分布式数据结构**时优势明显,但需权衡其学习成本和性能开销。对于简单查询或字符串操作,可搭配轻量级客户端使用。

          乐观锁和悲观锁是数据库和并发编程中常用的两种锁机制,它们各有优缺点,适用于不同的场景。

          乐观锁

          优点:

          1. 高并发性能:乐观锁不使用数据库锁机制,而是通过版本号或时间戳等机制来实现,减少了锁的开销,适合读多写少的场景。

          2. 无死锁:由于不使用数据库锁机制,避免了死锁的发生。

          3. 适合分布式系统:在分布式系统中,乐观锁可以减少锁的粒度,提高系统的整体性能。

          缺点:

          1. 冲突处理复杂:在高并发写操作的场景下,冲突的概率较高,需要处理冲突的逻辑较为复杂。

          2. 重试成本高:当发生冲突时,需要进行重试操作,可能会导致性能下降。

          悲观锁

          优点:

          1. 数据一致性高:悲观锁通过锁定数据来保证数据的一致性,适合写多读少的场景。

          2. 简单易用:悲观锁的实现相对简单,适合对数据一致性要求较高的场景。

          缺点:

          1. 性能开销大:悲观锁需要频繁地加锁和解锁操作,增加了系统的开销,降低了并发性能。

          2. 容易产生死锁:在复杂的并发场景下,容易产生死锁问题,需要额外的机制来检测和处理死锁。

          3. 不适合分布式系统:在分布式系统中,悲观锁的实现较为复杂,性能也不如乐观锁。

          适用场景

          • 乐观锁适用于读多写少的场景,如大部分查询操作较多的系统。

          • 悲观锁适用于写多读少的场景,如银行转账等对数据一致性要求较高的系统。

          通过了解乐观锁和悲观锁的优缺点,可以根据具体的业务需求选择合适的锁机制,以提高系统的性能和数据一致性。


          http://www.hkcw.cn/article/UAsJZAuAnc.shtml

          相关文章

          软件测试环境搭建与测试流程

          &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1.软件测试环境搭建 思考&#xff1a; 在什么条件下做软件测试&#xff1f;怎么做软件测试&#xff1f; 1.1 搭建测试环境前 确定测试目的 功能测试&#xff…

          Go语言学习-->从零开始搭建环境

          Go语言学习–>从零开始搭建环境 1 开发环境 Go官网下载地址&#xff1a;https://golang.org/dl/ Go官方镜像站&#xff08;推荐&#xff09;&#xff1a;https://golang.google.cn/dl/ windos 平台下载&#xff1a; 我这里下载1.22稳定版 双击下载好的.msi文件 修改安装…

          Mac 芯片系列 安装cocoapod 教程

          安装声明&#xff1a; 本人是在搭梯子的环境下安装成功&#xff0c;前提是必须安装好安装homebrew环境。 1.检测rudy的源 2.查看源(目的:检测rudy的源) gem sources - l 3.移除源(目的:移除rudy自带的源) gem sources --remove https://rubygems.org/ 4.更换源(目的:替换成国…

          idea不识别lombok---实体类报没有getter方法

          介绍 本篇文章&#xff0c;主要讲idea引入lombok后&#xff0c;在实体类中加注解Data&#xff0c;在项目启动的时候&#xff0c;编译不通过&#xff0c;报错xxx.java没有getXxxx&#xff08;&#xff09;方法。 原因有以下几种 1. idea没有开启lombok插件 2. 使用idea-2023…

          JavaWeb是什么?总结一下JavaWeb的体系

          一&#xff1a;Maven 1.1 定义 不需要导入依赖&#xff0c;在配置文件描述配置信息&#xff0c;maven会自动导入 统一项目结构&#xff1a; 项目构建&#xff1a; 1.2 ideal集成 &#xff08;1&#xff09;maven配置 &#xff08;2&#xff09;创建maven项目 ‘ &#xff08;3&…

          MEMCPY引发的非对齐访问

          目录 前言背景代码直接运行的现象问题原因解决办法 前言 1&#xff0c;memcpy在keil中是伪装成函数的C语言关键字&#xff0c;有可能会被优化为字对齐形式__rt_memcpy_w 2&#xff0c;编译到memcpy位置的时候&#xff0c;编译器会检查地址类型&#xff0c;如果两个指针都是4字…

          CSS(2)

          文章目录 Emmet语法快速生成HTML结构语法 Snipaste快速生成CSS样式语法快速格式化代码 快捷键&#xff08;VScode&#xff09;CSS 的复合选择器什么是复合选择器交集选择器后代选择器(重要)子选择器(重要&#xff09;并集选择器(重要&#xff09;**链接伪类选择器**focus伪类选…

          AI+在线教育系统源码:开发智能化互动网校平台全流程详解

          在数字化浪潮席卷各行各业的当下&#xff0c;教育行业也不例外。从最早的PPT网课&#xff0c;到今天“AI互动教学”深度融合的智能网校系统&#xff0c;在线教育平台的底层逻辑和技术架构已然发生翻天覆地的变化。今天&#xff0c;笔者将为正计划进入在线教育领域的创业者、产品…

          Prj09--8088单板机C语言8253产生1KHz方波(1)

          1.8253原理图 2.Deepseek给出的参考程序 #include <stdio.h> #include <conio.h> #include <dos.h>// 8253定时器端口定义 #define PORT_8253_CNT0 0x9000 // 计数器0地址 #define PORT_8253_CNT1 0x9001 // 计数器1地址 #define PORT_8253_CNT2 …

          女儿回应48岁妈妈顺产得子 意外怀孕引发热议

          女儿回应48岁妈妈顺产得子。近日,广东河源一位48岁的再婚妈妈怀孕7个月自己都没察觉,还以为是“绝经发福”了。她28岁的女儿透露,妈妈和现任丈夫相识仅40天就闪婚,已经共同生活了7年。此前医生曾诊断这位妈妈没有卵泡无法怀孕,没想到却意外怀上了。女儿最初心情复杂,但现…

          js-day7

          JS学习之旅-day7 1.事件流1.1 事件流与两个阶段说明1.2 事件捕获1.3 事件冒泡1.4 阻止1.5 解绑事件 2. 事件委托3. 其他事件3.1 页面加载事件3.2 页面滚动事件3.3 页面尺寸事件 4. 元素尺寸与位置 1.事件流 1.1 事件流与两个阶段说明 事件流指的是事件完整执行过程中的流动路…

          【Typst】5.文档结构元素与函数

          概述 本节介绍Typst文档的核心文档结构元素及其对应函数&#xff0c;还有函数的用法。通过本节你将可以更好的使用脚本创建和控制页面元素。 系列目录 1.Typst概述2.Typst标记语法和基础样式3.Typst脚本语法4.导入、包含和读取5.文档结构元素与函数6.布局函数 set语句和sho…

          每日八股文6.3

          每日八股-6.3 Mysql1.COUNT 作用于主键列和非主键列时&#xff0c;结果会有不同吗&#xff1f;2.MySQL 中的内连接&#xff08;INNER JOIN&#xff09;和外连接&#xff08;OUTER JOIN&#xff09;有什么主要的区别&#xff1f;3.能详细描述一下 MySQL 执行一条查询 SQL 语句的…

          【Linux】pthread多线程同步

          参考文章&#xff1a;https://blog.csdn.net/Alkaid2000/article/details/128121066 一、线程同步 线程的主要优势在于&#xff0c;能够通过全局变量来共享信息。不过&#xff0c;这种便携的共享是有代价的&#xff1b;必须确保多个线程不会同时修改同一变量&#xff0c;或者某…

          Spring AOP:面向切面编程 详解代理模式

          文章目录 AOP介绍什么是Spring AOP&#xff1f;快速入门SpringAop引入依赖Aop的优点 Spring Aop 的核心概念切点(Pointcut)连接点、通知切面通知类型PointCut注解切面优先级Order切点表达式executionwithinthistargetargsannotation自定义注解 Spring AOP原理代理模式&#xff…

          Ubuntu20.04用root(管理员身份)启动vscode

          1.终端输入 code --user-data-dir~/.vscode --no-sandbox .

          第7章 :面向对象

          一、面向对象 面向过程&#xff1a;关心的是我该怎么做&#xff0c;一步步去实现这个功能 面向对象&#xff1a;关心的是我该让谁去做&#xff0c;去调用对象的操作来实现这个功能 面向对象 对象&#xff08;Object&#xff09; 分类&#xff08;Classification&#xff09; …

          嵌入式linux八股文

          1.指针的大小 指针大小的计算方式。指针的大小并非由其类型决定&#xff0c;而是与编译器的位数相关。具体来说&#xff0c;在 32 位的系统下&#xff0c;指针的大小为 4 个字节&#xff0c;而在 64 位的系统下&#xff0c;指针的大小为 8 个字节。通过示例代码的运行&#xf…

          python可视化:端午假期旅游火爆原因分析

          python可视化&#xff1a;端午假期旅游火爆原因分析 2025年的旅游市场表现强劲&#xff1a; 2025年端午假期全社会跨区域人员流动量累计6.57亿人次&#xff0c;日均2.19亿人次&#xff0c;同比增长3.0%。入境游订单同比大涨近90%&#xff0c;门票交易额&#xff08;GMV&#…

          ubuntu22.04安装taskfile

          sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -dsudo mv ./bin/task /usr/local/bin/测试 task --version