本文转载自微信公众号「Java3y」,作者Java3y。转载本文请联系Java3y公众号。
我,三歪,最近要开始写项目系列文章。我给这个系列取了一个名字,叫做《揭秘》
没错,我又给自己挖了个坑。
为什么想写项目相关的文章呢?原因有以下:
- 当我还没正式开始工作时,我经常会想:”网上的视频项目我是看过了,但真正的商业项目究竟长什么样?会不会很难?“我是挺想知道真正的商业项目跟自己练习的项目区别在哪。我估摸还没工作的同学应该也有跟我类似的思考吧?
- 变相推动自己持续输出,在这个过程中学习和成长。关注我可能有小白,也可能有跟我做同一领域的大佬。我把我所了解的写下来:可能我这边的实现方案被大佬们唾弃,交流和学习后,改善了我系统的实现方案。也有可能给正准备踏进该领域的同学提供一些参考价值。岂不美哉?
这个系列就以「消息管理平台」来打个样吧,这是我维护近一年的系统了。这篇文章可以带你全面认识「消息管理平台」是怎么设计和实现的,有兴趣的同学欢迎在评论区下留言和交流。
这篇文章可能稍微会有些许长,我是打算一篇就把该系统给讲清楚。「消息管理平台」原理并不难,没有很多专业名词,实现起来也不会复杂,你要是觉得学到了,欢迎给我点个赞👍
简单认识《消息管理平台》
「消息管理平台」可能在不同的公司会有不同的叫法,有的时候我会叫它「推送系统」,有的时候我会叫它「消息管理平台」,也有的同事叫它「触达平台」,甚至浮夸点我也可以叫它「消息中台」
但是不管怎么样,它的功能就是给用户发消息。在公司里它是怎么样的定位?只要以官方名义发送的消息,都走消息管理平台。
一般你注册一个APP/网站,你可以收到该APP/网站给你发什么消息呢?一般就以下吧?
- 站内信(IM)消息:其实就是APP内聊天的消息
- 通知栏(PUSH)消息:系统弹窗消息
- 邮件(Email)消息
- 短信(Sms)消息
- 微信服务号消息
- 微信小程序(服务通知)消息
好了,我相信你已经知道这个系统是用来干嘛的了。那为什么要有这个系统呢?
为什么要有消息管理平台?
可以说,只要是做APP的公司几乎都会有消息管理平台。
我们很多时候都会想给用户发消息:
- 有可能是用户想要这样的功能(预约活动提醒通知)
- 也有可能是我们想通过发消息来「唤醒」/「告知」等操作,告诉用户我们还在(大爷来玩啊)
那么问题来了,发消息困难吗?发消息复杂吗?
显然,发消息非常简单,一点儿也不复杂。
发短信无非就是调用第三方短信的API、发邮件无非就是调用邮件的API、发微信类的消息(手Q/小程序/微信服务号)无非就是调用微信的API、发通知栏消息(Push)无非就是调APNS/手机厂商的API、发IM消息也可以使用云服务,调云服务的API...
可能很多人的项目都是这么干的,无非发条消息,自己实现也不是不可以。
但这样会带来的问题就是在一个公司内部,会有很多个项目都会有「发送消息」的代码实现。假设发消息出了问题,还得去自己解决。
首先是系统不好维护,其次是没必要。我一个搞广告的,虽然我要发消息,凭什么要我自己去实现?
我们在写代码时,可能会把公用的代码抽成方法,供当前的项目重复调用。如果该公用的代码被多个项目使用,可能我们又会抽成组件包,供多个项目使用。只要该公用的代码被足够多的人去用,那它就很有可能从组件上升为一个平台(系统)级的东西。
如何实现消息管理平台?
回到消息管理平台的本质,它就是一个可以发消息的系统。那怎么设计和实现呢?我们从接口说起吧。
接口设计
消息管理平台是一个提供消息发送服务的平台,如果让我去实现,我的想法可能是把每种类型的消息都写一个接口,然后把这些接口对外暴露。
所以,可能会有以下的接口:
- /**
- * content:发送的文案
- * receiver:接收者
- */
- sendSms(String content,String receiver);
- sendIm(String content,String receiver);
- sendPush(String content,String receiver);
- sendEmail(String content,String receiver);
- sendTencent(String content,String receiver);
- //....
这样实现好像也不是不可以,反正每个接口都挺清晰的,要发什么类型的消息,你调用哪个接口就好了。
假设我们定义了如上的接口,现在我们要发消息了,我们会有以下的场景:
a.文案:「你好,我是三歪」,接收人:「woshisanwai」 (一次只发给一个人)
b.文案:「你好,我是三歪」,接收人:「woshisanwai,java3y,javayyy」(相同的文案发给多个人)
假如你是新手,你可能会想:这简单,我每种类型分开两个接口,分别是单发和批量接口。
- sendSingleSms();
- sendBatchSms();
- //...
上面这样设计有必要吗?其实没啥必要。我将接收人定义为一个Array不就得了?Array的size==1,那我就把该文案发给这个人,Array的size>1,那我就把这个文案发给Array里边的所有人。
所以我们的接口还是只有一个:
- /**
- * content:发送的文案
- * receiver:接收者(可多个,可单个)
- */
- sendSms(String content,Set<String> receiver);
其实在我们这也不是定义Array,我的接口receiver仍然是String,如果有多个用,号分隔就可以了。
- /**
- * content:发送的文案
- * receiver:接收者(可多个,可单个),多个用逗号分隔开
- */
- sendSms(String content,String receiver);
现在还有个场景,不同的文案发给不同的人怎么办?有的人就说,这不已经实现了吗?直接调用上面的接口就完事了啊。你又不是不能重复调用,比如说:
a.文案:「你好,我是Java3y」,接收人:「woshisanwai」
b.文案:「你好,我是三歪」,接收人:「3y」
c.文案:「你好,woshisanwai」,接收人:「三歪」
d......
确实如此,本来就可以这样做的。但不够好
举个真实的场景:现在有一个主播开播了,得发送一条消息告诉订阅该主播的人赶紧去看。为了提高该条通知的效果 ,在文案上我们是这样设计的:{用户昵称},你订阅的主播三歪已经开播了,赶紧去看吧!
这种消息我们肯定是要求实时性的(假设推送消息的速度太慢了,等到用户收到消息了,主播都下播了,那用户不得锤死你?)
画外音:显然这种情况属于不同的文案发给不同的人
这种消息在业务层是怎么做的呢?可能是扫DB表,遍历出订阅该主播的粉丝,然后给他们推送消息。
那现在我们只能每扫出一个订阅该主播的粉丝,就得调用send()接口发送消息。如果该主播有500W的粉丝,那就得调用500W次send接口,这不是很可怕?这调用次数,这网络开销...
于是乎,我们得提供一个“批量”接口,可以让调用方一次传入不同文案所携带不同的人。那怎么做呢?也很简单,实际上就是上面接口再封装一层,让调用方能“批量”传进来就好了。所以代码可以是这样的:
- /**
- * 一次传入多个(文案以及发送者)的“组”进来
- * List<SendParam>
- * SendParam 里边 定义了 content 和receiver
- */
- sendBatchSms(List<SendParam> sendParam);
现在接口的“雏形”已经出现了,到这里我们实现了消息管理平台最基本的功能:发消息
我们先不管内部的实现是如何,假设我们已经适配好调通好对应的API了,现在我们的接口在发消息层面上已经有充分必要的条件了:只要你传入接收者和发送内容,我就可以给你发消息。
但我们对外称可是一个平台啊,怎么能搞得像是只封装了几个方法似的,平台就该有平台的样子。
我举个日常最最最基本的功能:有人调用了我的接口发了条短信,这条短信的文案是一条内容为验证码类型,他问我这条短信到底下发到用户手上了没有。
如果接入过短信的同学就会知道:发送短信到用户收到是一个异步的过程
- 调用短信提供商的API,假设你的入参没有问题,它会告诉你“调用”成功。你想真正地知道此条内容到底有没有下发到用户手上,你有两种办法:一、提供一个接口给短信服务商调用,等真正处理完了,短信服务商会调用你的接口,告诉你最终的结果是什么。二、你去轮询短信服务商的接口,获取最终的结果。
回到问题上,他想要他调用我的接口有没有把短信发送成功,那我只要问他拿到手机号和文案,然后有以下步骤:
a.判断该手机号和文案在下发时是否正常(有没有真正调用下发短信的接口)
b.假设调用短信接口下发成功,那看下返回的回执(下发结果)是否正常
那目前我们在现有的接口,还是很完美地支持上面的问题的,对吧?只要我们记录了下发的结果和回执的信息,我们就可以告诉他所提供的手机号和文案究竟有没有下发到用户手上。
那今天他又过来问了:今天有很多人来反馈收不到验证码短信(不是全部人收不到,是大部分人),我想了解一下今天验证码短信下发的成功率是多少。
此时的我,只能去匹配(like %%)他的文案调用我的接口下发了多少人,调用短信服务商的API下发成功多少人,收到的成功回执(结果)有多少人。
通过匹配文案的方式最终也是可以告诉他结果的,但是这种是很傻X的做法。归根到底还是因为系统提供的服务还是太薄弱了。
那怎么解决上面所讲的问题呢?其实也很简单,既然匹配文案很傻X,那我给他这一批验证码的短信取个唯一的Id那不就可以了吗?
像我们去接入短信服务商一样,我们需要去新建一个短信模板,这个模板代表了你要发送的内容,新建模板后会给你个模板Id,你下发的时候指定这个模板Id就好了。
那我们的平台也可以这样玩啊,你想发消息对吧?可以,先来我的平台新建一个”模板“,到时候把模板Id发给我就行。
于是,我们就完美地解决上面所提到的问题了。
我们现在再来讨论一下有没有必要不同的消息类型(短信、邮件、IM等)需要分开不同的的接口,其实是没必要的了。因为只要抽象了”模板“这个概念,消息类型自然我们就可以在模板上固化掉,只要传了模板Id,我就知道你发的是什么类型消息。
这样一来,我们最终会有两个接口:批量与单个发送接口。
- /**
- * 发送消息接口
- * @author java3y
- */
- public interface SendService {
- /**
- * 相同文案,发给0~N 人
- * @param sendParam
- */
- void send(SendParam sendParam);
- /**
- * 不同文案,发给不同人,一次可接收多组
- * @param sendParam
- */
- void batchSend(BatchSendParam sendParam);
- }
- public class SendParam {
- /**
- * 模板Id
- */
- private String templateId;
- /**
- * 消息参数
- */
- private MsgParam msgParam;
- }
- public class MsgParam {
- /**
- * 接收者:假设有多个,则用「,」分隔开
- */
- private String receiver;
- /**
- * 自定义参数(文案)
- */
- private Map<String, String> variables;
- }
单个接口指的是:一次给1~N人发送消息,这批人收到的是相同的文案
批量接口指的是:一次给1个人发送一个文案,但一次调用可以传N个人及对应的文案
这里的单个和批量不是以发送人的维度去定义的,而是人所对应的消息文案。
再再再举个例子,现在我给关注我的同学都发一条消息:「大哥大嫂新年好」,这种情况我只需要使用send方法就好了,相同的文案我给一批人发,这批人收到的文案是一模一样的。
一次单推接口调用的请求参数:
- {
- "templateId": 12345,
- "msgParam":
- {
- "receivers": "三歪,敖丙,鸡蛋,米豆",
- "variables": {
- "content": "大哥大哥新年好",
- "title": "来个赞吧,亲"
- }
- }
- }
如果我要给关注我的同学都发一条消息:「{微信用户名},大哥大哥新年好」,这种情况我一般用batchSend方法,在发送之前组合人所对应的文案封装成一个List,一次调用接口对调用方而言就是一次发了List.size()组人。
一次批量接口调用的请求参数:
- {
- "templateId": 12345,
- "msgParam": [
- {
- "receivers": "敖丙",
- "variables": {
- "content": "敖丙,大哥大哥新年好",
- "title": "来个赞吧,亲"
- }
- },
- {
- "receivers": "鸡蛋",
- "variables": {
- "content": "鸡蛋,大哥大哥新年好",
- "title": "来个赞吧,亲"
- }
- }
- ]
- }
没想到单单接口这块我这篇就写了这么长,主要是照顾没有经验的同学哈~
回顾设计接口的思路:
a.起初是想每种消息类型分开不同的接口
b.考虑到同一个文案会下发给多个人,所以接收者参数得是支持”批量“的传入
c.考虑到会有批量调用接口的场景,所以需要一个批量接口
d.考虑到需要统计下发消息的场景,所以需要抽象出”模板“,在平台下发的消息都得有”模板“
e.有了”模板“,可以将很多信息固化到模板中,所以最终我们抽象出两个接口:单推和批量。
再来聊聊模板
在前面我们已经定义好接口了,跟简单你们所实现的发消息功能最主要的区别就是多了”模板“的概念。
在上面提到了一点:有了”模板“,可以将很多信息固化到模板中。那我们固化了什么东西到模板中呢?
- 能够发送的消息种类。消息管理平台是可以发多种类型的消息的,所以我们模板是需要有字段区分不同的消息类型。别想得这么难,其实我们就用1表示短信,2表示邮件...
- 模板创建者信息(手机号、姓名),这个跟发消息的实质内容没有任何关系,只是如果模板出现了什么不可描述的问题,背锅侠总得找出来吧,如果模板创建者离职了怎么办?没事,我会根据创建者把所在部门给找到,那就找部门背锅(嘿嘿)
- 消息的文案。综合上面所看到的消息,我们可以看到一条消息无非由以下部分所组成:内容、标题、图片、链接、视频...不同的消息能发的文案也不一样,像短信顶多就只有内容和链接,而像通知栏消息(Push)就可以有标题、内容、图片、链接所组成。所以,我们会把消息的文案用json的格式存储在一个字段中。
- 消息的业务规则。这里所讲的业务规则并不是真正的细节业务,而是对不同消息类型上的平台性约束。比如说,在产品层面上,希望晚上用户收不到通知栏推送(毕竟会对用户进行打扰);希望用户一个小时内不会接收到两条,一天最多收到N条通知栏推送(也是出于用户的体验)。这些平台性的约束就适合放在消息管理平台上做,你可以理解为是一个兜底的功能。
- 发送账号。什么?发条消息还有账号的概念?你搞错了吧,三歪?。其实是真的有的,在发邮件的时候可以选取不同的邮件账号,在发微信公众号消息时可以选取不同的微信公众号(小程序同理),在发IM消息时可以使用不同的账号发送。而在接入短信的时候其实是分了两种类型的:通知和营销。我们会把这些都抽象为账号。
- 接收者Id类型。站内的IM消息用的是站内的userId,发通知栏消息(PUSH)用的是did,发短信用的是手机号,发微信类的消息用的是openId。指定接收者的Id类型,表明这个模板你要传入哪种类型的id。假设你指明是userId,但你要发短信,消息管理平台就需要将userId转成手机号。这里也是用一个字段标识,1表示userId,2表示did ...
可以发现的是,我们把一条消息所需要的信息(甚至不需要的信息)都塞进模板里面了,等调用方传入模板Id时,我就能拿到我想要的所有信息了。
这是一个模板的全部了吗?当然不是咯。上面提到的是模板共性的内容,我们按模板的使用场景还划分两种类型:
- 运营模板:运营要给指定一批人在某时某刻发送消息。(这一批人是T+1离线的)。例子:如果用户注册登录了APP,可以隔一天(甚至更长时间)给用户发消息。这种属于非实时(离线)推送,这种就不需要技术来承接,去圈选人群后设置对应的时间即可推送。
- 技术模板:系统根据业务条件自动触发一批消息,接收者名单也依赖业务场景(这批人一般是实时的)。例子:如果用户注册登录了APP,就立马需要给该用户发消息。这种属于实时推送,需要对应的技术来承接。
随着系统和业务的演进,运营模板和技术模板的界限会越来越模糊。从本质上就是提供了两种发消息的方式:
a.圈定一批人群,通过使用定时任务到点调用接口触发(接收者、文案、发送时间都已明确)。
b.技术调用接口发送消息(接收者,文案,发送时间均由业务逻辑所产生)。例子:欢迎关注三歪,你的验证码是:888。有内鬼,终止交易。(当你关注三歪时,系统触发一条消息。发送时间、验证码值、人员均不确定)
用户在平台创建模板时,不同类型的模板需要填写的字段是不一样的:运营模板需要填写人群和任务触发时间,而技术模板压根就不需要填人群和任务触发时间,所以我们模板会有一个字段标识该模板是运营类型还是技术类型。1表示运营类型,2表示技术类型...
你觉得已经完了吗?nonono,还没有。我们还会区分消息的类型,目前最主要由三类组成:通知、营销和验证码。
问题来了,为什么我们要区分消息的类型呢?做统计用吗?当然不是了,就这几个粒度的类型有什么好统计的。
还是以例子来说明吧:在2020-02-30日,运营同学圈选了一个5000W的人群选择在晚上8点发送一条短信,大致的情况就是告诉用户三歪文章更新了,不看血亏。系统在晚上8点准时执行任务,读取该模板的模板信息下发。5000W人,系统能秒发吗?显然是不行的
画外音:除了考虑自身的系统能力,还得考虑下游能承受的能力。你瞎搞,人家就不带你玩了。
所以,这5000W人肯定是需要一定的时间才能完全下发的,现在我们假设是15分钟完全下发完毕吧。在8点2分触发了一条验证码的短信,结果因为这个5000W的人群所导致验证码的消息延迟发送,这合理吗?显然不合理。
怎么导致的?原因是这5000W的消息和验证码的消息走的是同一个通道,导致验证码的消息被阻塞掉了。我们将不同的消息类型走不同的通道,就可以解决掉上面的问题。
所以,我们的系统在设计层面上就把运营模板默认设置为营销类型的消息,而技术模板的消息类型由调用者自行选择。在现实场景中,能堵的就只有营销类的消息。
画外音:上面所讲的这些实践都是跟使用场景和具体业务所关联的,肯定不是一朝一夕就可以全想出来的。
- 模板也已经聊完了,还有些细节的东西我这就不赘述了。我再来简要总结一下:
- 我们把发送一条消息所必要的信息(文案、发送账号、传入的接收者Id类型、消息类型:通知、营销和验证码)、平台性的信息(业务规则:是否去重、屏蔽、展示逻辑等)和基本信息(业务方信息、消息名称)全都塞到模板中
由于使用场景,模板会分为运营模板和技术模板。运营模板主要的特点是需要填写人群信息和发送时间,运营模板由消息管理平台自身进行调度发送消息。
接口实现
BB了这么久了,可能很多人只是想来看看:三歪这逼在标题还敢还写个揭秘,发消息谁不会,不就调个API嘛,还能给你玩出花来?
别急嘛,现在就写。前面已经铺垫了接口的设计和模板究竟是什么了,现在我们还是回到接口的实现上吧。
首先我们简单来看看消息管理平台的系统架构链路图:
画外音:上面我们所说的接口定义在统一调用层(接入层)中
调用者调用我们的send/batchSend方法,会直接调用下游的API下发消息吗?不会
直接调用下游的API下发消息风险太大了,接口1W+QPS都是很正常的事,所以我们接收到消息后只是做简单的参数校验处理和信息补全就把消息发到消息队列上。这样做的好处就是接口接入层十分轻量级,只要Kafka抗得住,请求就没问题。
发到消息队列时,会根据不同的消息类型发到不同的topic上,发送层监听topic进行消费就好了。架构大致如下:
发送层消费topic后,会把消息放在各自的内存队列上,多个线程消费内存队列的消息来实现消息的下发。
可以看到的是:从接入层发到消息队列上我们就已经做了分topic来实现业务上的隔离,在消费时我们也是放到各自的内存队列中来进行消费。这就实现了:不同渠道和同渠道的不同类型的消息都互不干扰。
看到上面这张图,如果思考过的同学肯定会问:这要内存队列干啥啊?反正你在上层已经分了topic了,不用内存队列也可以实现你所讲的“业务隔离”啊。
也的确,这里使用内存队列的主要原因是为了提高并发度。提高了并发度,这意味着下发速度可以更快(在下发消息的过程中,最耗时的还是网络交互,像短信这种可以多开点线程进行消费)。
在前面所提到的业务规则就是在下发层这儿做的,包括夜间屏蔽、1小时去重和Id转换等
- 夜间屏蔽就是判断是否在晚上,如果勾选了夜间屏蔽并且在晚上,过滤掉就好了
- 1小时去重就是拿userId+消息渠道作为Key,看是否存在Redis上,假设存在,则过滤掉
- id转换这功能我们做成了个系统,这块我放在下面简单说一下吧,这就不在赘述了。
画外音:这种场景最好使用Pipeline来读写Redis
随后就是适配各个渠道的接口,调用API下发消息了,这块就跟你们单个的实现没什么大的区别了,调用个接口还能给你玩出花来?(代码风格会稍好一些,模板方法模式、责任链、生产者与消费者模式等在项目中都有对应的应用)
总结一下接口的实现:
a.调用方调用接口时,接口不会同步直接调用下游的API发送消息,而是放入消息队列上(支持高并发)
b.放入队列时,会根据不同渠道以及不同类型的消息进行分类,放到不同的topic(业务隔离)
c.消费队列时,会在本地使用阻塞队列来提高并发度(加快消费的速度)
Id转换
(扩展)在前面也提到了,发不同类型的消息会需要有不同的id类型:微信类需要openId、短信需要手机号、push通知栏推送需要did。
在大多数情况下,一般调用者就传入userId给到我,我这边需要根据不同的消息类型对userId进行转换。
那在我们这边是怎么实现该系统的呢?主要的步骤和逻辑有以下:
a.监听用户变更和微信公众号订阅/取关的topic,在Flink清洗出一个统一的数据模型,将清洗后的数据写到另一个的topic。
b.Id映射系统监听Flink清洗出的topic,实时写到数据源(这里我们用的是搜索引擎)
看着也不会很难,对吧?
有没有想过一个问题,为什么要用一个Id映射系统去监听Flink洗出来的topic,而不是在Flink直接写到数据源呢?
其实通过Flink直接写到数据源也是完全没问题的,而封装了一个Id映射系统,就可以把这活做得更细致。
从描述可以发现的是:在上面只实现了实时增量。很多时候我们会担心增量存在问题,导致部分数据的不准确或者丢失,都会写一份全量,Id映射也是同样的。
那Id映射的全量是怎么做的呢?用户数据通过各种关联关系会在Hive形成一张表,而Id映射的全量就是基于这张Hive表来实现全量(每天凌晨会读取Hive表的信息,再写一遍数据源)。
基于上面这些逻辑,专门给Id映射做了个后台管理(可以手动触发全量、是否开启增量/全量、修改全量触发的时间)
数据统计
我觉得这块是消息管理平台最最最精华的一部分。
梦回我们当初的接口设计环节,我们就是因为有“数据统计”的需求,才引入了模板的概念。现在我们已经有了一个模板Id了,在我们这边是怎么实现数据的统计的呢?我们对消息的统计都是基于模板的维度来实现的。
在创建模板时就会有一个模板Id生成,基于这个模板Id,我们生成了一个叫做umpId的值:第一位分为技术/运营推送,最后八位是日期,中间六位是模板Id
因为所有的消息都会经过接入层,只要消息带有链接,我们就会给链接后加上umpid参数,链接会一直下发透传,直至用户点击
每个系统在执行消息的时候都会可能导致这条消息发不出去(可能是消息去重了,可能是用户的手机号不正确,可能是用户太久没有登录了等等都有可能)。我们在这些『关键位置』都打上日志,方便我们去排查。
这些「关键位置」我们都给它用简单的数字来命个名。比如说:我们用「11」来代表这个用户没有绑定手机号,用「12」来代表这个用户10分钟前收到了一条一模一样的消息,用「13」来代表这个用户屏蔽了消息.....
「11」「12」「13」「14」「15」「16」这些就叫做「点位」,把这些点位在关键的位置中打上日志,这个就叫做「埋点」
有了埋点,我们要做的就是将这些点位收集起来,然后统一处理成我们的数据格式,输出到数据源中。
a.收集日志
b.清洗日志
c.输出到数据源
有logAgent帮我们收集日志到Kafka,实时清洗日志我们用的是Flink,清洗完我们输出到Redis(实时)/Hive(离线)。
Hive表的数据样例(主要用于离线报表统计):
Redis会以多维度来进行存储,以便支撑我们的业务需要。比如,要查一条消息为何发送失败,通过userId搜一下,直接完事(实时的都记录在Redis中,所以这里读取的是Redis的数据)
比如,通过模板Id,查某条消息的整体下发情况:
为什么我说这是消息管理平台最最最精华的呢?umpId贯穿了所有消息管理平台经过的系统,只要是在消息管理平台发的消息,都会被记录下来发送,可以通过点位来快速追踪消息的下发情况。
总结一下数据统计:
a.设计出业务上的umpid,给所有的消息推送链接都加上umpdId 参数
b.打通上下游,共同设计和维护关键点位,统一日志格式来实现跨平台的收集和清洗
c.兼顾实时和离线需求写到不同的数据源,实时以多维度统计来快速定位问题
聊聊运营层
面前面提到了,运营的模板是需要圈选一批人群,然后下发消息的,那这群人从哪里来?
在很久之前,消息管理平台也把人群给做掉了,大致的思路就是可以支持文件上传和hivesql上传两种方式去圈选人群,圈出来上传到hdfs进行读取,支持对人群的更新/切分/导出等功能。
有了人群的概念,你会发现你收到的消息其实都是跟你息息相关的(不是瞎给你推送的,你在里面,才能圈到你)。可能是因为你看了几天的连衣裙,所以给你推送连衣裙的消息,吸引去你购买。
后来,由于公司内部DMP系统崛起,人群就都交由DMP给管理了。但实现的思路也都是类似的,只不过还是同样的:人家做的是平台,功能肯定比会自己写几个接口要完善不少。
做推送就免不了发错了消息,特别是在运营侧(分分钟就推送千万人),我们平台又做了什么措施去尽可能避免这种问题的发生呢?
在运营圈定人群后,我们会有单独的测试功能去「测试单个用户」是否能正常下发消息,文案链接是否存在问题。
这一个步骤是必须要做的,给用户发出的消息,首先要经过自己的校验。如果确认链接和文案都无问题后,则提交任务,走工单审批后才能发送。
如果在启动之后发现文案/链接存在问题,还可以拦截剩余未发的消息。
针对于(技术方推送),我们在预发环境下配置了「白名单」才能收到消息。
- 线上消息有「去重」的逻辑:
- 在某段时间内,过滤掉重复消息
运营类消息推送(圈定人群的方式去下发消息)同一个用户需要相隔一段时间才能下发一次。
虽然说,我们制定了很多的规则去尽量避免事故的发生,但不得不说推送还是一个容易出现事故的功能。我的牛逼已经吹完了,如果某天发现我的推送出了事故,不要@我,当没见过这篇文章就好。
总结
不知道大家看完之后觉得消息管理平台难不难,从理解上的角度而言,这系统应该是很好理解的,没有掺杂很多业务的东西,都是做平台性相关的内容。
这个系统能支持数W的QPS,每天亿级的流量推送,一篇文章也不可能把消息管理平台的所有功能点都讲完,内容也远不止上面这些,但核心我应该是讲清楚的了。
发送消息可以做得很简单,也可以做得很平台化,如果你觉得你学到了些许东西,希望可以给我点个在看和转发一波。如果你对我写的内容有疑问,欢迎评论区交流。