背景
事情是这样的,前几天up主在跟一个面试官聊天,聊到消息队列kafka,然后提到,怎样实现消息的延迟发送。当时内心状态是这样的。
哈哈哈,怎么可能,武功再高也怕菜刀,当时就上演了一出乱拳打死老师傅的戏码。
不扯了,言归正传,当时的思路是这样的,延迟发送么,把kafka换掉不就行了,rocketMQ都帮你实现好了,哈哈哈。是不是既简单又粗暴。但是这样说出来又显得UP主无能,这不行,才高八斗,玉树临风,风靡万千少女的我怎么可能吃瘪,不可能,绝对不可能
当时就想到,不就延时么,加个时间就可以了么,搞一张数据表,数据丢进去,加个时间,轮询,发送,结束!NO NO NO
细细想来就会发现问题,消息少了还行,多了就不行了,还会卡性能,灵光一现,Redis是个好东西啊,基于内存,读写性能强大到爆炸,就它了。
Redis的物种数据结构中有一种是ZSET数据结构,非常适合现在的这种场景,在这种数据结构中每个元素都有score和value,我们可以将消息产的时间作为score,这样在Redis中的消息就是根据时间排序的有序集合,每次在里面取出时间戳最小的消息消费即可,消费结束删除,到这里基本思路就出来了,直接上代码,开搞!
引入基础包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
获取延时消息(代码仅供延时,提供思路)
/**
* 获取延时队列中已到期的消息
*/
public List<String> getExpiredMessages() {
//获取到开始时间
long minScore = 0;
//获取到结束时间
long maxScore = System.currentTimeMillis();
//获取到指定范围区间的数据列表
Set<Object> messages = Collections.singleton(redis.zRangeByInx("KEY", minScore, maxScore));
if (messages == null || messages.isEmpty()) {
return Collections.emptyList();
}
List<String> result = new ArrayList<>();
for (Object message : messages) {
String delayMessage = JSONObject.parseObject(JSON.toJSONString(message), DelayMessage.class);
result.add(delayMessage);
}
return result;
}
这样基本功能就有了,开启一个定时任务,直接在池子里取数据就行了,大概实现就是,当消息产生的时候,就将时间戳作为score放到ZSET集合里面,消费消息在里面按照时间先后顺序取出,消费完删除,看到这里是不是就觉得可以了啊,完美!NO NO NO
上面只是实现了最基础的功能,仔细观察你会发现,当数据量多的时候,消息的消费就会变慢,甚至会非常慢,因为上面的设计思路是串行执行的,如果生产者生成了一条消息,二消费者在半小时后才收到
所以要解决上面的问题,就需要解决消息消费慢的问题,更好的支持业务,那就需要引入线程池,并发消费,十个人干活总比一个人干活要快吧(不一定),废话不多说,上代码
List<String> messages =getExpiredMessages();
List<List<String>> partition = Lists.partition(messages, 10);
List<CompletableFuture<List<SectionOutputDTO>>> completableFutureList = partition.stream().map(
message -> CompletableFuture.runAsync(() ->
sendMessage(message),
taskExecutor)).collect(Collectors.toList());
CompletableFuture<Void> allOf = CompletableFuture.allOf(completableFutureList.toArray(new CompletableFuture[0]));
allOf.thenRun(()->{
redis.rovome(messages);
})
这样并发处理消息,而且引入单独的线程池,不会影响主系统流程,而且每次消费完都会清除已经消费的消息,防止了重复消费,按照这种思路搞起码不会出什么大问题。
重点
1.redis的ZSET数据结构特点
2.消息消费尽可能实时,延迟发送也要准确
3.redis的读写性能
4.并发控制
5.消息重复消费问题
思考
最后问下小伙伴,如果说不用redis还有其他实现方式吗,评论区打出来