Redisson分布式锁和延迟队列

Redisson

一、分布式锁

https://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw==&mid=2247545794&idx=2&sn=88a3b1c73372006b49a43a6c133a10c3&chksm=fbb1ba3cccc6332ae1f5e609ab5e37c32fe972b7ba3a18b1a92735c5f3e7c9300e2318ca2280&scene=27

1、自己写的分布锁
// 加锁
public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
    return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}
// 解锁
public void unlock(String lockName, String uuid) {
    if(uuid.equals(redisTemplate.opsForValue().get(lockName)){        redisTemplate.opsForValue().del(lockName);    }
}

存在问题:

  1. 这种写法是非原子性的,想要原子性还是使用lua脚本方便

  2. 但是自己写的锁是不可重入,可以参考synchronized实现可重入的分布式锁,JAVA对象头MARK WORD中便藏有线程ID和计数器来对当前线程做重入判断,避免每次CAS。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁标志是否设置成1:没有则CAS竞争;设置了,则CAS将对象头偏向锁指向当前线程。
再维护一个计数器,同个线程进入则自增1,离开再减1,直到为0才能释放
  1. 业务里总是有很多特殊情况的,比如A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题
  2. 负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态
2、使用redisson(只说他的非公平锁)
@Resource
private RedissonClient redissonClient;
RLock rLock = redissonClient.getLock(lockName);
try {
    boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS);
    if (isLocked) {
        // TODO
    }
} catch (Exception e) {
    rLock.unlock();
}

RLock

RLock是Redisson分布式锁的最核心接口,继承了concurrent包的Lock接口和自己的RLockAsync接口,RLockAsync的返回值都是RFuture,是Redisson执行异步实现的核心逻辑,也是Netty发挥的主要阵地。

image-20230726115322381

leaseTime != -1时:

image-20230726115423646

可以看到在删除锁的时候步骤如下:

  1. 如果该锁不存在则返回nil;

  2. 如果该锁存在则将其线程的hash key计数器-1,

  3. 计数器counter>0,重置下失效时间,返回0;否则,删除该锁,发布解锁消息unlockMessage,返回1;

其中unLock的时候使用到了Redis发布订阅PubSub完成消息通知。

而订阅的步骤就在RedissonLock的加锁入口的lock方法里

long threadId = Thread.currentThread().getId();
        Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
        if (ttl != null) {
            // 订阅
            RFuture<RedissonLockEntry> future = this.subscribe(threadId);
            if (interruptibly) {
                this.commandExecutor.syncSubscriptionInterrupted(future);
            } else {
                this.commandExecutor.syncSubscription(future);
            }
            // 省略

当锁被其他线程占用时,通过监听锁的释放通知(在其他线程通过RedissonLock释放锁时,会通过发布订阅pub/sub功能发起通知),等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段。

通知后又做了什么,进入LockPubSub。

这里只有一个明显的监听方法onMessage,其订阅和信号量的释放都在父类PublishSubscribe,我们只关注监听事件的实际操作

protected void onMessage(RedissonLockEntry value, Long message) {
        Runnable runnableToExecute;
        if (message.equals(unlockMessage)) {
            // 从监听器队列取监听线程执行监听回调
            runnableToExecute = (Runnable)value.getListeners().poll();
            if (runnableToExecute != null) {
                runnableToExecute.run();
            }
            // getLatch()返回的是Semaphore,信号量,此处是释放信号量
            // 释放信号量后会唤醒等待的entry.getLatch().tryAcquire去再次尝试申请锁
            value.getLatch().release();
        } else if (message.equals(readUnlockMessage)) {
            while(true) {
                runnableToExecute = (Runnable)value.getListeners().poll();
                if (runnableToExecute == null) {
                    value.getLatch().release(value.getLatch().getQueueLength());
                    break;
                }
                runnableToExecute.run();
            }
        }
    }
发现一个是默认解锁消息 ,一个是读锁解锁消息
//LockPubSub监听最终执行了2件事
runnableToExecute.run() 执行监听回调
value.getLatch().release(); 释放信号量
Redisson通过LockPubSub 监听解锁消息,执行监听回调和释放信号量通知等待线程可以重新抢锁。

leaseTime == -1时:

image-20230726115614384

watchDog解决任务执行超时但未结束,锁已经释放的问题。

当一个线程持有了一把锁,由于并未设置超时时间leaseTime,Redisson默认配置了30S,开启watchDog,每10S对该锁进行一次续约,维持30S的超时时间,直到任务完成再删除锁。

总体流程

  1. A、B线程争抢一把锁,A获取到后,B阻塞
  2. B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息
  3. A操作完成释放了锁,B线程收到订阅消息通知
  4. B被唤醒开始继续抢锁,拿到锁

二、延迟队列

1、自己写的延迟队列
//添加任务
redisTemplate.opsForZSet().add(DELAYED_QUEUE_KEY, task, timestamp);

 // 处理到期的任务
while (true) {
  long currentTimestamp = System.currentTimeMillis();
  Set<String> tasks = redisTemplate.opsForZSet().range(DELAYED_QUEUE_KEY, 0, currentTimestamp);
  if (!tasks.isEmpty()) {
    for (String task : tasks) {
      // 处理任务
      System.out.println("Processing task: " + task);
      // 从有序集合中移除已处理的任务
      redisTemplate.opsForZSet.remove(DELAYED_QUEUE_KEY, task);
    }
  }
  Thread.sleep(1000); // 每秒轮询一次
}

存在问题:

  1. 线程安全问题:多个线程同时访问zset队列,可能会导致消息重复消费,可以通过加锁解决。
  2. 通过的Thread.sleep(1000)让线程阻塞,会降低实时性。如果需要更高的实时性,可以考虑使用异步非阻塞的方式,可以使用Redis的发布/订阅功能或异步任务框架。
  3. 应该使用连接池来管理Redis连接,并确保在使用完毕后正确释放连接资源。

使用redisson延迟队列

RBlockingQueue<Long> blockingQueue = redissonClient.getBlockingQueue("ATTENDANCE");
RDelayedQueue<Long> delayQueue = redissonClient.getDelayedQueue(blockingQueue);
//添加消息
delayQueue.offer(taskId, seconds, timeUnit);
//消费消息
while (true) {
  Long taskId = null;
  try {
    taskId = blockingQueue.take();
    if (taskId != null){
      log.info("ATTENDANCE接收到延迟任务:{}", taskId);
    }
    Thread.sleep(100);
  }catch (Exception e) {
    e.printStackTrace();
  }
image-20230726163929454

Redisson的延迟队列实现原理是基于Redis的有序集合(Sorted Set)和发布/订阅(Pub/Sub)机制。

当你使用Redisson创建延迟队列时,它会在Redis中创建两个数据结构:一个有序集合用于存储延迟消息,一个普通队列用于存储待处理的消息。

  1. 添加延迟消息:当你使用RDelayedQueue.offer()方法将延迟消息添加到延迟队列时,Redisson会将消息的到期时间(即延迟时间加上当前时间)作为分值,消息内容作为成员,将消息添加到有序集合中。

  2. 处理延迟消息:Redisson会启动一个后台线程,定期轮询有序集合,检查是否有消息的到期时间已经到达。如果有到期的消息,Redisson会将这些消息从有序集合中移除,并将它们添加到普通队列中。

  3. 消费消息:你可以使用普通队列的RQueue.poll()方法来消费队列中的消息。当你调用该方法时,Redisson会从普通队列中弹出一条消息并返回给你。

  4. 消息重试:如果消息处理失败或需要重试,你可以将消息重新添加到延迟队列中,以便在未来的某个时间再次处理。你可以使用RDelayedQueue.offer()方法指定新的延迟时间。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值