问题背景
很多人可能会留意到, 关注了公众号之后,隔一段时间, 公众号会推送消息出来,打开消息后发现这些消息看起来不像人工发送的,应该是设计好的一套关注后的定时推送机制, 从而来达到获客转化的目的.
我司微星极光的一个重磅产品, 公众号激活宝, 目前就以第三方平台的形式给各个服务号公众号提供了这样的赋能.
效果如下图
本文主要介绍如何实现这种推送机制的技术方案
技术选型思路
定时调度数据库轮询
这种是很容易想到方案, 有点是简单粗暴, 缺点也同样明显, 效率低下, 适合在用户量很少的时候,顶一顶. 一般草根团队在PoC阶段凑和用的, 我们微信极光公众号激活宝这个产品, 开发这个功能的时候, 已经积累了非常多的用户量, 这里显然不合适, 所以pass掉了
使用golang defer timmer
他们第三方平台的后台语言使用Golang, 这个也是一开始尝试过的方案, 非常灵巧, 见效快.
代码语言:txt复制timer = time.AfterFunc(time.Duration(n) * time.Minute, func() {
// do something
})
defer timer.Stop()
但是这个方案有2个缺点
- 进程重启后, 定时的调度信息丢失.
- 不支持分布式, 有时候会有重复推送.
所以虽然实现起来超级简单, 但可用性没达标, 最后pass
使用Linux时间轮
golang defer timmer的底层是linux时间轮, 为了解决分布式的本质问题, 研究了时间轮的实现.
最简单的时间轮, 可以拿钟表比喻, 比如有的任务固定在15分钟的时候触发, 那么这些任务就排队盯着15分钟这个维度, 等着时间点到, 就触发链表挨个调用函数, 如下图
上面的简单模型有一个问题, 就是刻度是均匀的, 而且范围也有限, 而实际情况中呢, 我们会有不同的刻度(时间粒度)计时, 而且时间范围跨度也会很大.这种情况下像是个水表, 不同粒度有着不同的触发周期.时间以固定 tick 递增,而当前时间指针则也依次递增,如果发现当前指针的位置可以确定为一个注册的定时器,就触发其注册的回调函数.
linux的时间轮数据结构,如下, 内核的定时器本质上是 Single-Shot Timer,如果想成为 Repeating Timer,可以在注册的回调函数中再次的注册自己
结论, 时间轮是一切定时任务的核心底层逻辑, 但是对于应用层来说, 显然用起来还是不够方便, 应该有更易于使用的形式存在.
使用AWS的SQS消息队列服务
AWS的SQS提供delay的支持, 非常完美得解决了这个问题,
接口调用也很简单
代码语言:txt复制System.out.println("Sending a message with a 5-second timer to MyQueue.n");
SendMessageRequest request = new SendMessageRequest(myQueueUrl, "This is my message text.");
request.setDelaySeconds(5);
sqs.sendMessage(request);
有点是调用简单, 一个月有100万条消息的免费额度,
缺点是超过配额之后, 费用还是挺贵的.
使用redis的redis key notification功能
Redis在2.8版本以后, 支持了一个特性, 叫Keyspace Notifications.
通过Pub/Sub的机制, 设置TTL, 然后另外的进程来监控Expired事件, 从而达到delay message的目的.
值得注意的是, 官方文档有这样的描述.
Because Redis Pub/Sub is fire and forget currently there is no way to use this feature if your application demands reliable notification of events, that is, if your Pub/Sub client disconnects, and reconnects later, all the events delivered during the time the client was disconnected are lost.
Redis提供的这个事件回调,并不承诺可靠.
使用RabbitMq实现延时队列
AMQP协议和RabbitMQ队列本身没有直接支持延迟队列功能,但是可以通过以下特性模拟出延迟队列的功能.
- TTL(Time to Live)
- DLX(Dead Letter Exchanges)
简单而言就是对每条消息设置TTL, TTL就是延迟的秒数. 然后消息到期后, 进入死信队列,
死信队列里面再进行规则转发.
原理就是这么个原理, 这个需求挺通用的, RabbitMq官方也给出了一个插件rabbitmq-delayed-message-exchange
.
使用起来在header里面x-delay
加上延迟的秒数就行
headers.put("x-delay", 5000)
实战方案
在实际的过程中呢, 我们最先使用的是AWS的SQS, 后来业务起来了,发现免费的100万一个月不够用, bill的账单数字还挺吓人的, 于是改成自研的方案.
因为历史积累, 我们重度使用redis, 于是入坑了redis的keyspace notifications的方案, sofar so good, 直到有2个问题暴露出来,
一个就是官方文档提到的, reconnect的之后, 不保证可靠性, 这个监控显示有概率非常小发生, 不到十万分之一, 对比了机器的环境, 应该是和网络抖动有关, 这个小概率的修复, 目前我们吞下了.
第二个问题就是因为redis是内存型的, 如果有一些公众号粉突然爆粉的时候, 会导致redis的key暴增, 进而导致内存暴增. 而redis并不支持弹性伸缩, 导致一度非常蛋疼.经常收到告警, 半夜爬起来手工处理.
第二个问题后来通过keydb来解决了, keydb 里面有个flash功能, 非常切合我们的业务, 业务其实我们并不需要很高的内存, 把这些key扔到flash disk里面, 可以大大缓解内容压力, 从而只需要加大磁盘容量, 减少redis的实例数目, 节省了很大的成本. 而key的的通知回调, 时间其实并不敏感, 我们的推送迟个几秒钟, 就算迟发1分钟, 其实也好好. 只要不是不发就行, 于是这个方案目前在成本和可靠性方面, 提供一个最优解.
参考链接
https://www.ibm.com/developerworks/cn/linux/l-cn-timers/index.html
https://aws.amazon.com/sqs/pricing/
https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-send-message-with-timer.html
https://redis.io/topics/notifications
https://docs.keydb.dev/docs/notifications/
https://www.cloudamqp.com/docs/delayed-messages.html
https://wxjiguang.com
本文连接
https://www.wxjiguang.com/blog/how-to-push-message-with-timmer-after-user-subscribed-in-wechat-official-account/