实现一个直播前一小时推送开课通知的功能的几种方案

2,748 阅读7分钟

1、 业务场景的引出

你现在在开发一个直播管理的模块。在新增一条直播课程记录的时候,需要实现一个直播前一小时推送开课通知的功能。比如新增表单页设置的开课时间是晚上7点,那么晚上6点的时候,你的服务就得发送一条请求到微信小程序服务,然后它给关注的老师们发送上课提醒。

2、 思考落地方案 —— 扫表

然后你开始思考,实现这样的一个功能,该怎么办。首先是第一种方案:定时任务+定时扫表方案。

你的项目是SpringBoot框架,你写了个方法,加个注解,配置每分钟方法执行一次,然后在方法里去查出直播课程记录表中的状态为未直播的课程,然后判断它的时间是不是可以发送开课通知了。比如开课时间是晚上8点,现在7点1分了,开课时间-当前时间<1小时,然后直接发送请求到微信小程序服务,让其给该老师发送开课通知。

差不多一上午,你就写完这个功能了。然后测了测,没有bug。但是你突然觉得,这种实现方式不是很优雅,很low。比如说:假如现在表里有几万条未直播的课程,那你定时任务一分钟执行一次,性能太差了,会给你的mysql造成很大压力;另外就是可能同样的一条记录,可能这次还没处理完,下一次的定时任务的线程就又处理到这条记录了,会不会造成一些线程不安全的问题。

你觉得问题很严重,那么我把定时任务的执行时间延长行不行啊,这样一来,可能就不会有太大的问题了。然后你把定时任务改成每5分钟执行一次,可是测试后发现,这样会造成一个问题,那就是会造成开课提醒有很大的延时性,比如说,之前你的定时任务是每分钟执行一次,那么8点的课程,你发送开课提醒的时间大概是6点59到7点1分之间,这样一来用户还可以接受;然后你现在是定时任务5分钟执行一次,那么发送开课提醒的时间就变成6点55分-7点5分了,这样其实延时性很大了。用户接不接受先放一边,产品经理可以接受吗?

3、 思考落地方案——Timer

然后你想了想,延时性确实也是个问题,那么,用jdk的Timer怎么样,针对每一个直播课程,你创建的时候,都生成一个Timer,然后休眠一定时间后去执行任务。比如你创建了一个直播课程,它的开课时间是晚上8点,当前时间是下午6点,8点-6点-1= 1小时。你把课程的ID当成参数传给Timer,然后它的休眠时间你传进去1小时。然后你刷刷刷,又花了2个小时把这个功能重构了。然后测试,没有问题,太完美了,7点准时发的开课通知,几乎没有时延性。

虽然这种实现方式性能上比扫表强,也解决了时延性问题,可是很快你又觉得这种实现方式还是有问题。什么问题呢?首先来讲吧,如果我的后台管理员,提前一个礼拜就创建了直播课程记录,那么我的线程是不是得休眠一个礼拜啊,他可以休眠那么久而不被垃圾回收吗?还有就算它可以休眠一个礼拜甚至一个月,那么万一你的管理员赶上个假日,比如国庆节,他想的挺好,8号有很多直播,然后它9月30号创建了1千条,那么1000个线程就在那阻塞着,你觉得你的系统的性能能不收影响吗?

那么用jdk自带的延时队列DelayQueue呢?你想了想,觉得还是不行,为啥呢,因为DelayQueue也是适用于量小的情况。

4、 思考落地方案——Rocketmq延时队列

反正现在考虑的是将来的性能,那么你引入消息中间件怎么样呢?你把创建的课程的ID发到一个延时队列里,然后有个线程作为消费者去消费消息,RocketMq里恰好有延时队列的功能,也完全不用担心后期的数据量大的性能问题了,用RocketMq完全可以支持这种数据量。

可是后边你发现了一个很尴尬的问题,你发现RocketMq支持特定level的延时消息,什么意思呢,就是这个延时时间是固定的,比如10分钟,15分钟,30分钟,这些值都是固定的,如果你现在写的是一个订单服务,那么用户没有及时支付,你可以把订单自动取消,就是你生成这个订单的时候,把订单ID发到延时队列里,延时时间就可以选择半个小时,那么半个小时后,消费者线程去消费该消息的时候,你拿到订单ID去表里一查,如果订单的状态是未支付,那你就处理消息,把这条订单的状态改为取消;如果订单的状态是已支付,那你就丢掉该消息,不作处理。

可是你现在的推送时间是动态的啊!比如你7点8分创建的任务,9点直播,那么等于是52分钟后你就得发送通知。这个时间不是固定的啊,很可惜,这种业务场景下你用不了RocketMq。唉。

5.思考落地方案——RabbitMq延时队列

现在既然打算用消息中间件的延时队列功能了,那么用不了RocketMq,用RabbitMq的延时队列功能怎么样。你发现,RabbitMq的延时队列支持的时间精度是任意的,不是像RocketMq必须是特定的level。可是,你发现了一个缺点:使用RabbitMQ来实现延迟任务队列的时候,需要确保业务上每个任务的延迟时间是一致的。如果遇到不同的任务类型需要不同的延时的话,需要为每一种不同延迟时间的消息建立单独的消息队列。啥意思呢,就是比如我新建的2个课程都是8点直播,那这两个消息就单独创建一个队列放进去,我新建的3个课程是8点半直播,那我还得创建另一个队列把消息放进去。

你想了想,觉得虽然可能需要频繁的创建队列,但是使用RabbitMQ的延时队列来实现业务需求,确实是可以的。即可以满足当下,又可以满足未来数据量激增带来的性能问题的解决。

6、 还有别的实现方式吗——redis延时队列

那还有别的实现方案吗,还真的有,redis的延时队列也可以满足这个功能。Redis的 list列表数据结构常用来作为异步消息队列使用,例如实现一个先进先出的队列,我们可以使用lpush/rpop或者rpush/lpop这样对称的命令,push会把数据塞到列表中,pop则会从列表中移除数据。这样实现上要比RabbitMQ的延时队列简单点。

7、 总结

如果你对自己的代码质量很高,和考虑后期的维护性,你最好不要用定时任务+定时扫表或JDK Timer来实现;如果你现在的项目没有用到MQ。只用到redis了,那你可以考虑用redis的延时队列来实现该功能,因为毕竟引入一个新组件到服务,是要谨慎的。

作者简介:豪横的小耳朵,一个豪横的程序员。想和大家一起在技术的世界里豪横,用技术的眼光去看待世界。欢迎扫描下方二维码,持续关注,一大波原创系列文章正在路上。

关注后回复“666”,可免费获取java高级工程师学习资料一份。