技术学习总结
学习技术一定要制定一个明确的学习路线,这样才能高效的学习,不必要做无效功,既浪费时间又得不到什么效率,大家不妨按照我这份路线来学习。
最后面试分享
大家不妨直接在牛客和力扣上多刷题,同时,我也拿了一些面试题跟大家分享,也是从一些大佬那里获得的,大家不妨多刷刷题,为金九银十冲一波!
@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;
总结
总的来说,面试是有套路的,一面基础,二面架构,三面个人。
最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友
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)]