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); }
}
存在问题:
-
这种写法是非原子性的,想要原子性还是使用lua脚本方便
-
但是自己写的锁是不可重入,可以参考synchronized实现可重入的分布式锁,JAVA对象头MARK WORD中便藏有线程ID和计数器来对当前线程做重入判断,避免每次CAS。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁标志是否设置成1:没有则CAS竞争;设置了,则CAS将对象头偏向锁指向当前线程。
再维护一个计数器,同个线程进入则自增1,离开再减1,直到为0才能释放
- 业务里总是有很多特殊情况的,比如A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题 。
- 负责储存这个分布式锁的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发挥的主要阵地。
leaseTime != -1时:
可以看到在删除锁的时候步骤如下:
-
如果该锁不存在则返回nil;
-
如果该锁存在则将其线程的hash key计数器-1,
-
计数器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时:
watchDog解决任务执行超时但未结束,锁已经释放的问题。
当一个线程持有了一把锁,由于并未设置超时时间leaseTime,Redisson默认配置了30S,开启watchDog,每10S对该锁进行一次续约,维持30S的超时时间,直到任务完成再删除锁。
总体流程
- A、B线程争抢一把锁,A获取到后,B阻塞
- B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息
- A操作完成释放了锁,B线程收到订阅消息通知
- 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); // 每秒轮询一次
}
存在问题:
- 线程安全问题:多个线程同时访问zset队列,可能会导致消息重复消费,可以通过加锁解决。
- 通过的Thread.sleep(1000)让线程阻塞,会降低实时性。如果需要更高的实时性,可以考虑使用异步非阻塞的方式,可以使用Redis的发布/订阅功能或异步任务框架。
- 应该使用连接池来管理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](https://i-blog.csdnimg.cn/blog_migrate/0c51a16bcac07eb80c302413a6acc4d7.png)
Redisson的延迟队列实现原理是基于Redis的有序集合(Sorted Set)和发布/订阅(Pub/Sub)机制。
当你使用Redisson创建延迟队列时,它会在Redis中创建两个数据结构:一个有序集合用于存储延迟消息,一个普通队列用于存储待处理的消息。
-
添加延迟消息:当你使用
RDelayedQueue.offer()
方法将延迟消息添加到延迟队列时,Redisson会将消息的到期时间(即延迟时间加上当前时间)作为分值,消息内容作为成员,将消息添加到有序集合中。 -
处理延迟消息:Redisson会启动一个后台线程,定期轮询有序集合,检查是否有消息的到期时间已经到达。如果有到期的消息,Redisson会将这些消息从有序集合中移除,并将它们添加到普通队列中。
-
消费消息:你可以使用普通队列的
RQueue.poll()
方法来消费队列中的消息。当你调用该方法时,Redisson会从普通队列中弹出一条消息并返回给你。 -
消息重试:如果消息处理失败或需要重试,你可以将消息重新添加到延迟队列中,以便在未来的某个时间再次处理。你可以使用
RDelayedQueue.offer()
方法指定新的延迟时间。