微信搜【 Java3y】关注这个有梦想的男人,点赞关注是对我最大的支持!文本已收录至个人GitHub:https://github.com/ZhongFuCheng3y/3y,有300多篇原创文章,最近在连载面试和项目系列!java
我,三歪,最近要开始写项目系列文章。我给这个系列取了一个名字,叫作《揭秘》git
没错,我又给本身挖了个坑。github
为何想写项目相关的文章呢?缘由有如下:面试
这个系列就以「消息管理平台」来打个样吧,这是我维护近一年的系统了。这篇文章能够带你全面认识「消息管理平台」是怎么设计和实现的,有兴趣的同窗欢迎在评论区下留言和交流。sql
这篇文章可能稍微会有些许长,我是打算一篇就把该系统给讲清楚。「消息管理平台」原理并不难,没有不少专业名词,实现起来也不会复杂,你要是以为学到了,欢迎给我点个赞👍json
「消息管理平台」可能在不一样的公司会有不一样的叫法,有的时候我会叫它「推送系统」,有的时候我会叫它「消息管理平台」,也有的同事叫它「触达平台」,甚至浮夸点我也能够叫它「消息中台」小程序
可是无论怎么样,它的功能就是给用户发消息。在公司里它是怎么样的定位?只要以官方名义发送的消息,都走消息管理平台。微信小程序
通常你注册一个APP/网站
,你能够收到该APP/网站
给你发什么消息呢?通常就如下吧?微信
好了,我相信你已经知道这个系统是用来干吗的了。那为何要有这个系统呢?网络
能够说,只要是作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); //....
这样实现好像也不是不能够,反正每一个接口都挺清晰的,要发什么类型的消息,你调用哪一个接口就行了。
假设咱们定义了如上的接口,如今咱们要发消息了,咱们会有如下的场景:
假如你是新手,你可能会想:这简单,我每种类型分开两个接口,分别是单发和批量接口。
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);
如今还有个场景,不一样的文案发给不一样的人怎么办?有的人就说,这不已经实现了吗?直接调用上面的接口就完事了啊。你又不是不能重复调用,好比说:
确实如此,原本就能够这样作的。但不够好
举个真实的场景:如今有一个主播开播了,得发送一条消息告诉订阅该主播的人赶忙去看。为了提升该条通知的效果 ,在文案上咱们是这样设计的:{用户昵称},你订阅的主播三歪已经开播了,赶忙去看吧!
这种消息咱们确定是要求实时性的(假设推送消息的速度太慢了,等到用户收到消息了,主播都下播了,那用户不得锤死你?)
画外音:显然这种状况属于 不一样的文案发给不一样的人
这种消息在业务层是怎么作的呢?多是扫DB表,遍历出订阅该主播的粉丝,而后给他们推送消息。
那如今咱们只能每扫出一个订阅该主播的粉丝,就得调用send()
接口发送消息。若是该主播有500W
的粉丝,那就得调用500W
次send
接口,这不是很可怕?这调用次数,这网络开销...
因而乎,咱们得提供一个“批量”接口,可让调用方一次传入不一样文案所携带不一样的人。那怎么作呢?也很简单,实际上就是上面接口再封装一层,让调用方能“批量”传进来就行了。因此代码能够是这样的:
/** * 一次传入多个(文案以及发送者)的“组”进来 * List<SendParam> * SendParam 里边 定义了 content 和receiver */ sendBatchSms(List<SendParam> sendParam);
如今接口的“雏形”已经出现了,到这里咱们实现了消息管理平台最基本的功能:发消息
咱们先无论内部的实现是如何,假设咱们已经适配好调通好对应的API了,如今咱们的接口在发消息层面上已经有充分必要的条件了:只要你传入接收者和发送内容
,我就能够给你发消息。
但咱们对外称但是一个平台啊,怎么能搞得像是只封装了几个方法似的,平台就该有平台的样子。
我举个平常最最最基本的功能:有人调用了个人接口发了条短信,这条短信的文案是一条内容为验证码类型,他问我这条短信到底下发到用户手上了没有。
若是接入太短信的同窗就会知道:发送短信到用户收到是一个异步的过程
回到问题上,他想要他调用个人接口有没有把短信发送成功,那我只要问他拿到手机号和文案,而后有如下步骤:
那目前咱们在现有的接口,仍是很完美地支持上面的问题的,对吧?只要咱们记录了下发的结果和回执的信息,咱们就能够告诉他所提供的手机号和文案究竟有没有下发到用户手上。
那今天他又过来问了:今天有不少人来反馈收不到验证码短信(不是所有人收不到,是大部分人),我想了解一下今天验证码短信下发的成功率是多少。
此时的我,只能去匹配(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": "来个赞吧,亲" } } ] }
没想到单单接口这块我这篇就写了这么长,主要是照顾没有经验的同窗哈~
回顾设计接口的思路:
在前面咱们已经定义好接口了,跟简单大家所实现的发消息功能最主要的区别就是多了”模板“的概念。
在上面提到了一点:有了”模板“,能够将不少信息固化到模板中。那咱们固化了什么东西到模板中呢?
1
表示短信,2
表示邮件...json
的格式存储在一个字段中。userId
,发通知栏消息(PUSH)用的是did
,发短信用的是手机号,发微信类的消息用的是openId
。指定接收者的Id类型,代表这个模板你要传入哪一种类型的id
。假设你指明是userId
,但你要发短信,消息管理平台就须要将userId
转成手机号。这里也是用一个字段标识,1
表示userId
,2
表示did
...能够发现的是,咱们把一条消息所须要的信息(甚至不须要的信息)都塞进模板里面了,等调用方传入模板Id时,我就能拿到我想要的全部信息了。
这是一个模板的所有了吗?固然不是咯。上面提到的是模板共性的内容,咱们按模板的使用场景还划分两种类型:
T+1
离线的)。例子:若是用户注册登陆了APP,能够隔一天(甚至更长时间)给用户发消息。这种属于非实时(离线)推送,这种就不须要技术来承接,去圈选人群后设置对应的时间便可推送。随着系统和业务的演进,运营模板和技术模板的界限会愈来愈模糊。从本质上就是提供了两种发消息的方式:
用户在平台建立模板时,不一样类型的模板须要填写的字段是不同的:运营模板须要填写人群和任务触发时间,而技术模板压根就不须要填人群和任务触发时间,因此咱们模板会有一个字段标识该模板是运营类型仍是技术类型。1
表示运营类型,2
表示技术类型...
你以为已经完了吗?nonono,尚未。咱们还会区分消息的类型,目前最主要由三类组成:通知、营销和验证码。
问题来了,为何咱们要区分消息的类型呢?作统计用吗?固然不是了,就这几个粒度的类型有什么好统计的。
仍是以例子来讲明吧:在2020-02-30
日,运营同窗圈选了一个5000W
的人群选择在晚上8点发送一条短信,大体的状况就是告诉用户三歪文章更新了,不看血亏。系统在晚上8点
准时执行任务,读取该模板的模板信息下发。5000W
人,系统能秒发吗?显然是不行的
画外音:除了考虑自身的系统能力,还得考虑下游能承受的能力。 你瞎搞,人家就不带你玩了。
因此,这5000W
人确定是须要必定的时间才能彻底下发的,如今咱们假设是15分钟
彻底下发完毕吧。在8点2分
触发了一条验证码的短信,结果由于这个5000W
的人群所致使验证码的消息延迟发送,这合理吗?显然不合理。
怎么致使的?缘由是这5000W
的消息和验证码的消息走的是同一个通道,致使验证码的消息被阻塞掉了。咱们将不一样的消息类型走不一样的通道,就能够解决掉上面的问题。
因此,咱们的系统在设计层面上就把运营模板默认设置为营销类型的消息,而技术模板的消息类型由调用者自行选择。在现实场景中,能堵的就只有营销类的消息。
画外音:上面所讲的这些实践都是跟使用场景和具体业务所关联的,确定不是一朝一夕就能够全想出来的。
模板也已经聊完了,还有些细节的东西我这就不赘述了。我再来简要总结一下:
BB了这么久了,可能不少人只是想来看看:三歪这逼在标题还敢还写个揭秘,发消息谁不会,不就调个API嘛,还能给你玩出花来?
别急嘛,如今就写。前面已经铺垫了接口的设计和模板到底是什么了,如今咱们仍是回到接口的实现上吧。
首先咱们简单来看看消息管理平台的系统架构链路图:
画外音:上面咱们所说的接口定义在 统一调用层(接入层)中
调用者调用咱们的send/batchSend
方法,会直接调用下游的API下发消息吗?不会
直接调用下游的API下发消息风险太大了,接口1W+QPS
都是很正常的事,因此咱们接收到消息后只是作简单的参数校验处理和信息补全就把消息发到消息队列上。这样作的好处就是接口接入层十分轻量级,只要Kafka抗得住,请求就没问题。
发到消息队列时,会根据不一样的消息类型发到不一样的topic
上,发送层监听topic
进行消费就行了。架构大体以下:
发送层消费topic
后,会把消息放在各自的内存队列上,多个线程消费内存队列的消息来实现消息的下发。
能够看到的是:从接入层发到消息队列上咱们就已经作了分topic
来实现业务上的隔离,在消费时咱们也是放到各自的内存队列中来进行消费。这就实现了:不一样渠道和同渠道的不一样类型的消息都互不干扰。
看到上面这张图,若是思考过的同窗确定会问:这要内存队列干啥啊?反正你在上层已经分了topic
了,不用内存队列也能够实现你所讲的“业务隔离”啊。
也的确,这里使用内存队列的主要缘由是为了提升并发度。提升了并发度,这意味着下发速度能够更快(在下发消息的过程当中,最耗时的仍是网络交互,像短信这种能够多开点线程进行消费)。
在前面所提到的业务规则就是在下发层这儿作的,包括夜间屏蔽、1小时去重和Id转换等
userId+消息渠道
做为Key,看是否存在Redis上,假设存在,则过滤掉id转换
这功能咱们作成了个系统,这块我放在下面简单说一下吧,这就不在赘述了。画外音:这种场景最好使用 Pipeline来读写Redis
随后就是适配各个渠道的接口,调用API
下发消息了,这块就跟大家单个的实现没什么大的区别了,调用个接口还能给你玩出花来?(代码风格会稍好一些,模板方法模式、责任链、生产者与消费者模式等在项目中都有对应的应用)
总结一下接口的实现:
API
发送消息,而是放入消息队列上(支持高并发)在前面也提到了,发不一样类型的消息会须要有不一样的id
类型:微信类须要openId
、短信须要手机号、push通知栏推送须要did
。
在大多数状况下,通常调用者就传入userId
给到我,我这边须要根据不一样的消息类型对userId
进行转换。
那在咱们这边是怎么实现该系统的呢?主要的步骤和逻辑有如下:
topic
,在Flink
清洗出一个统一的数据模型,将清洗后的数据写到另外一个的topic
。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」这些就叫作「点位」,把这些点位在关键的位置中打上日志,这个就叫作「埋点」
有了埋点,咱们要作的就是将这些点位收集起来,而后统一处理成咱们的数据格式,输出到数据源中。
有logAgent帮咱们收集日志到Kafka,实时清洗日志咱们用的是Flink,清洗完咱们输出到Redis(实时)/Hive(离线)。
Hive表的数据样例(主要用于离线报表统计):
Redis会以多维度来进行存储,以便支撑咱们的业务须要。好比,要查一条消息为什么发送失败,经过userId
搜一下,直接完事(实时的都记录在Redis中,因此这里读取的是Redis的数据)
好比,经过模板Id,查某条消息的总体下发状况:
为何我说这是消息管理平台最最最精华的呢?umpId
贯穿了全部消息管理平台通过的系统,只要是在消息管理平台发的消息,都会被记录下来发送,能够经过点位来快速追踪消息的下发状况。
总结一下数据统计:
umpid
,给全部的消息推送连接都加上umpdId
参数前面提到了,运营的模板是须要圈选一批人群,而后下发消息的,那这群人从哪里来?
在好久以前,消息管理平台也把人群给作掉了,大体的思路就是能够支持文件上传
和hivesql
上传两种方式去圈选人群,圈出来上传到hdfs
进行读取,支持对人群的更新/切分/导出等功能。
有了人群的概念,你会发现你收到的消息其实都是跟你息息相关的(不是瞎给你推送的,你在里面,才能圈到你)。多是由于你看了几天的连衣裙,因此给你推送连衣裙的消息,吸引去你购买。
后来,因为公司内部DMP
系统崛起,人群就都交由DMP
给管理了。但实现的思路也都是相似的,只不过仍是一样的:人家作的是平台,功能确定比会本身写几个接口要完善很多。
作推送就免不了发错了消息,特别是在运营侧(分分钟就推送千万人),咱们平台又作了什么措施去尽量避免这种问题的发生呢?
在运营圈定人群后,咱们会有单独的测试功能去「测试单个用户」是否能正常下发消息,文案连接是否存在问题。
这一个步骤是必需要作的,给用户发出的消息,首先要通过本身的校验。若是确认连接和文案都无问题后,则提交任务,走工单审批后才能发送。
若是在启动以后发现文案/连接存在问题,还能够拦截剩余未发的消息。
针对于(技术方推送),咱们在预发环境下配置了「白名单」才能收到消息。
线上消息有「去重」的逻辑:
虽说,咱们制定了不少的规则去尽可能避免事故的发生,但不得不说推送仍是一个容易出现事故的功能。个人牛逼已经吹完了,若是某天发现个人推送出了事故,不要@我,当没见过这篇文章就好。
不知道你们看完以后以为消息管理平台难不难,从理解上的角度而言,这系统应该是很好理解的,没有掺杂不少业务的东西,都是作平台性相关的内容。
这个系统能支持数W的QPS,天天亿级的流量推送,一篇文章也不可能把消息管理平台的全部功能点都讲完,内容也不止上面这些,但核心我应该是讲清楚的了。
发送消息能够作得很简单,也能够作得很平台化,若是你以为你学到了些许东西,但愿能够给我点个在看和转发一波。若是你对我写的内容有疑问,欢迎评论区交流。
后续可能会更多写广告系统相关的内容,会以一些小的问题切入,不得不说,广告系统比消息管理平台仍是要复杂和有趣得多。提早关注预约最新文章,不会让你但愿的!
我是三歪,下期揭秘-广告系统再见
PDF文档的内容均为手打,有任何的不懂均可以直接来问我