Java最全Redisson延迟队列执行流程源码分析一下(1),面试题kafka数据丢失问题

技术学习总结

学习技术一定要制定一个明确的学习路线,这样才能高效的学习,不必要做无效功,既浪费时间又得不到什么效率,大家不妨按照我这份路线来学习。

最后面试分享

大家不妨直接在牛客和力扣上多刷题,同时,我也拿了一些面试题跟大家分享,也是从一些大佬那里获得的,大家不妨多刷刷题,为金九银十冲一波!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

@Slf4j

@Component

public class RedissonQueueHandle implements InitializingBean {

private final RBlockingQueue<RedisDataEntity<?>> queue;

private final RDelayedQueue<RedisDataEntity<?>> delayedQueue;

public RedissonQueueHandle (RedissonClient client) {

this.queue = client.getBlockingQueue(“redisson:queue”);

this.delayedQueue = client.getDelayedQueue(queue);

}

@Override

public void afterPropertiesSet () {

// 开一个线程阻塞式获取任务

thread();

// 使用netty时间轮循环获取任务

// watchDog(new HashedWheelTimer());

// 使用线程池定时获取任务

// schedule();

}

private void thread () {

new Thread(() -> {

while (true) {

try {

RedisDataEntity entity = queue.take();

log.info(“本次获取数据:{},耗时:{}”, entity, System.currentTimeMillis() - entity.getTime());

} catch (Exception e) {

}

}

}, “zrh”).start();

}

private void watchDog (final HashedWheelTimer timer) {

timer.newTimeout(timeout -> {

RedisDataEntity entity = queue.poll();

if (null != entity) {

log.info(“本次获取数据:{},耗时:{}”, entity, System.currentTimeMillis() - entity.getTime());

}

watchDog(timer);

}, 3, TimeUnit.SECONDS);

}

private void schedule () {

Executors.newScheduledThreadPool(1).scheduleWithFixedDelay(() -> {

RedisDataEntity entity = queue.poll();

if (null != entity) {

log.info(“本次获取数据:{},耗时:{}”, entity, System.currentTimeMillis() - entity.getTime());

}

}, 5, 5, TimeUnit.SECONDS);

}

/**

  • 放入redis,定时过期

  • @param entity

*/

public void offer (RedisDataEntity entity) {

try {

delayedQueue.offer(entity, entity.getExpire(), TimeUnit.MILLISECONDS);

} catch (Exception e) {

log.error(“放入redis延迟队列异常”, e);

}

}

}

复制代码

  • 放入redisson延迟队列可以是字符串也可以是对象RedisDataEntity,因为有进行IO磁盘存储操作,所以必须实现Serializable序列化接口。

/**

  • @Author: ZRH

  • @Date: 2022/1/10 11:54

*/

@Data

public class RedisDataEntity implements Serializable {

/**

  • 数据

*/

private final T data;

/**

  • 过期时间(单位:毫秒)

*/

private final Long expire;

/**

  • 添加时间

*/

private final Long time;

public RedisDataEntity (T data, Long expire, Long time) {

this.data = data;

this.expire = expire;

this.time = time;

}

}

复制代码

  • 然后开一个插入数据接口:

/**

  • @Author: ZRH

  • @Date: 2022/1/10 11:45

*/

@Slf4j

@RestController

public class IndexController {

private final RedissonQueueHandle redisHandle;

public IndexController (RedissonQueueHandle redisHandle) {

this.redisHandle = redisHandle;

}

@PostMapping(“redissonQueue”)

public String redissonQueue (@RequestParam String data, @RequestParam Long expire) {

RedisDataEntity entity = new RedisDataEntity(data, expire, System.currentTimeMillis());

log.info(“本次添加数据:{}”, entity);

redisHandle.offer(entity);

return “ok”;

}

}

访问接口设置延迟30秒:http://localhost:8802/redissonQueue?data=a&expire=30000,打印结果如下

2022-01-14 14:21:52.140 INFO 10808 — [nio-8802-exec-1] c.r.web.controller.IndexController : 本次添加数据:RedisDataEntity(data=a, expire=30000, time=1642141312135)

2022-01-14 14:21:52.887 INFO 10808 — [nio-8802-exec-2] c.r.web.controller.IndexController : 本次添加数据:RedisDataEntity(data=a, expire=30000, time=1642141312887)

2022-01-14 14:22:22.240 INFO 10808 — [ zrh] c.r.web.redis.RedissonQueueHandle : 本次获取数据:RedisDataEntity(data=a, expire=30000, time=1642141312135),耗时:30105

2022-01-14 14:22:22.914 INFO 10808 — [ zrh] c.r.web.redis.RedissonQueueHandle : 本次获取数据:RedisDataEntity(data=a, expire=30000, time=1642141312887),耗时:30027

复制代码

初始执行流程源码解析

  • redisson延迟队列最终都是和redis服务进行交互的,那可以使用monitor命令查看redis中执行了哪些命令,这样对了解其执行流程有很大帮助。

  • 上图是项目启动时,对redis发送的几个指令

  • “SUBSCRIBE”:订阅队列"redisson_delay_queue_channel:{redisson:queue}",里面有个定时任务通过该队列获取数据

  • “zrangebyscore”:获取"redisson_delay_queue_timeout:{redisson:queue}"集合中排序score值在0到1642148406748(当前时间戳)内的前100元素

  • “zrange”:获取"redisson_delay_queue_timeout:{redisson:queue}"集合中第一个元素,用于获取下一个元素的到期时间

  • “BLPOP”:取出并移除"redisson:queue"列表里的第一个元素,如果没有元素就一直等待阻塞。所以这里会阻塞着

  • “rpush”:如果指令"zrangebyscore"获取到了元素,那就将元素推送到队列redisson:queue内

  • “lrem”:如果指令"zrangebyscore"获取到了元素,那就删除队列"redisson_delay_queue:{redisson:queue}内元素为v的第一个元素

SUBSCRIBE指令

  • 进入RedissonDelayedQueue延迟队列的构造函数,里面就有上述执行指令的lua脚本命令(为了不影响篇幅删了一部分代码,下同):

protected RedissonDelayedQueue(QueueTransferService queueTransferService, Codec codec, final CommandAsyncExecutor commandExecutor, String name) {

super(codec, commandExecutor, name);

// list结构,用于延迟队列的订阅发布

channelName = prefixName(“redisson_delay_queue_channel”, getRawName());

// list结构,存放元素原始顺序

queueName = prefixName(“redisson_delay_queue”, getRawName());

// zset结构,存放未到期元素,并按照过期时间进行排好序

timeoutSetName = prefixName(“redisson_delay_queue_timeout”, getRawName());

QueueTransferTask task = new QueueTransferTask(commandExecutor.getConnectionManager()) {

@Override

protected RFuture pushTaskAsync() {

return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG,

"local expiredValues = redis.call(‘zrangebyscore’, KEYS[2], 0, ARGV[1], ‘limit’, 0, ARGV[2]); "

  • "if #expiredValues > 0 then "

  • "for i, v in ipairs(expiredValues) do "

  • “local randomId, value = struct.unpack(‘dLc0’, v);”

  • “redis.call(‘rpush’, KEYS[1], value);”

  • “redis.call(‘lrem’, KEYS[3], 1, v);”

  • "end; "

  • “redis.call(‘zrem’, KEYS[2], unpack(expiredValues));”

  • "end; "

// get startTime from scheduler queue head task

  • "local v = redis.call(‘zrange’, KEYS[2], 0, 0, ‘WITHSCORES’); "

  • "if v[1] ~= nil then "

  • "return v[2]; "

  • "end "

  • “return nil;”,

Arrays.asList(getRawName(), timeoutSetName, queueName),

System.currentTimeMillis(), 100);

}

@Override

protected RTopic getTopic() {

return RedissonTopic.createRaw(LongCodec.INSTANCE, commandExecutor, channelName);

}

};

queueTransferService.schedule(queueName, task);

this.queueTransferService = queueTransferService;

}

复制代码

  • 继续跟进queueTransferService.schedule(queueName, task)方法,因为第一次进入tasks集合,所以最后执行start()方法:

private final ConcurrentMap<String, QueueTransferTask> tasks = new ConcurrentHashMap<>();

public synchronized void schedule(String name, QueueTransferTask task) {

QueueTransferTask oldTask = tasks.putIfAbsent(name, task);

if (oldTask == null) {

task.start();

} else {

oldTask.incUsage();

}

}

复制代码

  • 进入QueueTransferTask,继续跟进schedulerTopic.addListener(…)方法:

private int messageListenerId;

private int statusListenerId;

public void start() {

RTopic schedulerTopic = getTopic();

statusListenerId = schedulerTopic.addListener(new BaseStatusListener() {

@Override

public void onSubscribe(String channel) {

pushTask();

}

});

messageListenerId = schedulerTopic.addListener(Long.class, new MessageListener() {

@Override

public void onMessage(CharSequence channel, Long startTime) {

scheduleTask(startTime);

}

});

}

复制代码

  • 然后会进入PublishSubscribeService.subscribe(…)方法:

  • 注意:这里继续调用重载方法subscribe(…)时设置了参数:PubSubType.SUBSCRIBE

public RFuture subscribe(Codec codec, ChannelName channelName, RedisPubSubListener<?>… listeners) {

return subscribe(PubSubType.SUBSCRIBE, codec, channelName, getEntry(channelName), listeners);

}

private RFuture subscribe(PubSubType type, Codec codec, ChannelName channelName, MasterSlaveEntry entry, RedisPubSubListener<?>… listeners) {

RPromise promise = new RedissonPromise<>();

AsyncSemaphore lock = getSemaphore(channelName);

// 创建一个线程任务放入lock对象

lock.acquire(() -> {

if (promise.isDone()) {

lock.release();

return;

}

subscribe(codec, channelName, entry, promise, type, lock, listeners);

});

return promise;

}

复制代码

  • AsyncSemaphore对象的acquire(…)方法会把线程任务放入自身队列listeners里,然后依次读取执行线程任务;

public class AsyncSemaphore {

private final AtomicInteger counter;

private final Queue listeners = new ConcurrentLinkedQueue<>();

public void acquire(Runnable listener) {

listeners.add(listener);

tryRun();

}

private void tryRun() {

if (counter.decrementAndGet() >= 0) {

Runnable listener = listeners.poll();

if (listener == null) {

counter.incrementAndGet();

return;

}

listener.run();

} else {

if (counter.incrementAndGet() > 0) {

tryRun();

}

}

}

}

复制代码

  • 然后继续跟进方法subscribe(codec, channelName, entry, promise, type, lock, listeners):

private void subscribe(Codec codec, ChannelName channelName, MasterSlaveEntry entry,

RPromise promise, PubSubType type,

AsyncSemaphore lock, RedisPubSubListener<?>… listeners) {

PubSubConnectionEntry connEntry = name2PubSubConnection.get(new PubSubKey(channelName, entry));

if (connEntry != null) {

addListeners(channelName, promise, type, lock, connEntry, listeners);

return;

}

freePubSubLock.acquire(() -> {

if (promise.isDone()) {

lock.release();

freePubSubLock.release();

return;

}

MasterSlaveEntry msEntry = Optional.ofNullable(connectionManager.getEntry(entry.getClient())).orElse(entry);

// 第一次进入entry2PubSubConnection集合为null,所以使用默认值,最后 freeEntry == null

PubSubEntry freePubSubConnections = entry2PubSubConnection.getOrDefault(msEntry, new PubSubEntry());

PubSubConnectionEntry freeEntry = freePubSubConnections.getEntries().peek();

if (freeEntry == null) {

freePubSubLock.release();

connect(codec, channelName, msEntry, promise, type, lock, listeners);

return;

}

});

}

复制代码

  • 继续跟进方法connect(codec, channelName, msEntry, promise, type, lock, listeners):

private void connect(Codec codec, ChannelName channelName,

MasterSlaveEntry msEntry, RPromise promise, PubSubType type, AsyncSemaphore lock, RedisPubSubListener<?>… listeners) {

RFuture connFuture = nextPubSubConnection(msEntry, channelName);

promise.onComplete((res, e) -> {…});

connFuture.onComplete((conn, ex) -> {

if (ex != null) {…}

freePubSubLock.acquire(() -> {

PubSubConnectionEntry entry = new PubSubConnectionEntry(conn, config.getSubscriptionsPerConnection());

int remainFreeAmount = entry.tryAcquire();

PubSubKey key = new PubSubKey(channelName, msEntry);

PubSubConnectionEntry oldEntry = name2PubSubConnection.putIfAbsent(key, entry);

if (oldEntry != null) {…}

if (remainFreeAmount > 0) {

addFreeConnectionEntry(channelName, entry);

}

freePubSubLock.release();

RFuture subscribeFuture = addListeners(channelName, promise, type, lock, entry, listeners);

ChannelFuture future;

// 这里通过上述重载方法传递的参数可知,最后走else逻辑

if (PubSubType.PSUBSCRIBE == type) {

future = entry.psubscribe(codec, channelName);

} else {

future = entry.subscribe(codec, channelName);

}

future.addListener((ChannelFutureListener) future1 -> {

if (!future1.isSuccess()) {…}

connectionManager.newTimeout(timeout ->

subscribeFuture.cancel(false),

config.getTimeout(), TimeUnit.MILLISECONDS);

});

});

});

}

复制代码

  • 该方法中支线内容不表述,主要看方法 entry.subscribe(codec, channelName),最后进入RedisPubSubConnection.async(…)方法,就是发送SUBSCRIBE指令的流程:

zrangebyscore和zrange指令

  • 订阅指令SUBSCRIBE发出后,在QueueTransferTask.start()方法里添加的监听器触发了,就会执行pushTask()

  • pushTaskAsync()方法执行完(lua脚本执行完),就会开启一个定时任务scheduleTask()

protected abstract RTopic getTopic();

protected abstract RFuture pushTaskAsync();

private void pushTask() {

// 这个抽象方法在之前构建RedissonDelayedQueue对象的构造函数里有实现,最后返回元素过期时间

RFuture startTimeFuture = pushTaskAsync();

startTimeFuture.onComplete((res, e) -> {

if (e != null) {

if (e instanceof RedissonShutdownException) {

return;

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

三面蚂蚁金服成功拿到offer后,他说他累了

三面蚂蚁金服成功拿到offer后,他说他累了

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

TaskAsync()方法执行完(lua脚本执行完),就会开启一个定时任务scheduleTask()

protected abstract RTopic getTopic();

protected abstract RFuture pushTaskAsync();

private void pushTask() {

// 这个抽象方法在之前构建RedissonDelayedQueue对象的构造函数里有实现,最后返回元素过期时间

RFuture startTimeFuture = pushTaskAsync();

startTimeFuture.onComplete((res, e) -> {

if (e != null) {

if (e instanceof RedissonShutdownException) {

return;

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

[外链图片转存中…(img-E04rN9TC-1715322533155)]

[外链图片转存中…(img-4HzznTbe-1715322533156)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值