2019独角兽企业重金招聘Python工程师标准>>>
最近上线了一个推送系统。推送系统作用是将短信,邮件,app push等消息触达到用户。目前功能上只实现了短信通道,并且随着业务量的扩大,还需要并行扩展推送能力。
公司的对推送系统的应用场景有两个,一个是主动发送,即在某一个时间点集中推送几万条短信。另一个是用户触发,通过上游系统接收到用户的行为而发送一条短信给用户。产品部门要求每条短信发送出去后,都要具体的执行报告:用户是否收到,收到的时间,失败的原因等等。所以系统就需要跟踪一次推出去的每条短信的发送状态。
目前系统接入了5个不同的短信网关。这个5个短信网关其中有4个是通过提供开发包(jar包形式),以socket的方式连接,一个是通过http协议访问,所以对于短信状态的处理也就有两种不同的形式。
第一种socket形式。这种开发包的处理方式一般是在第一次发送时与网关建立socket链接,双方在链接中进行通信。有的开发包里会自动维护该链接的状态,保证不会断掉,一直可以工作。而有的开发包则没有该项功能,需要客户端去维持这个链接。
如果系统中没有短信上行的需求,可以不做维持,在发送的时候判断一下链接是否可用,如果断掉,重新建立就是了。如果有上行的需求,恰巧连接断掉,那就会收不到网关推送过来的上行信息了,这种情况下必须维持。维持的方式一般都是定时去检查链接是否可用,如果不可用,立刻建立恢复。我们在接某个网关的时候,发现这个链接维持2个小时一般没有问题,如果单纯检查链接,调用方法返回是通的,但实际上已经不可用了,判断不是很准确,所以我们干脆不再去检查这个链接,而是每个一小时发一条短信,用正式的短信发送去维护他,这样比较省事。这里面还有一个小插曲,每小时发送的短信,开始时接收方设置成了10086,后来被网关方发现,说每小时给10086这样的号码发一条不大好,后来又设置成了公司内部的一个测试号码。
一般jar包中封装好了短信发送接口,短信报告回调接口,短信上行接口。要想追踪每条短信的发送状态,必须对回调接口的数据进行处理。
已其中一个短信网关的开发包为例。它的整个一条短信发送过程分成了3个处理部分:
一,发送。发送时会得到一个seqId,这个是在客户端标示短信的id
二,第一次回调,网关返回seqId和msgId 两个值。这次回调标志着短信已经进入网关的发送队列里,排队等待发送,msgId是在网关端唯一标示该短信的id
三,第二次回调,网关返回msgId和status,errMessage三个值,标志着该条短信最终的状态,用户是否收到出错原因等。
通过上面三个步骤可以看到,必须每次都要进行关联处理,才能保证最后的短信状态。
步骤 | seqId |
msgId |
status |
一 | √ | |
|
二 | √ | √ | |
三 | |
√ | √ |
我们的具体流程是:以一次发送10w条短信为例
一,10w端短信由其他系统插入到数据库中,作为一个发送task通知推送系统,每条短信都有一个自增长的smsId。
二,推送系统批量取出10w条短信,调用短信网关,每条短信都得到一个seqId。这样每条短信都需要根据smsId,更新seqId
三,第一次回调,每条短信要根据seqId,更新msgId
三,第二次回调,每条短信要根据msgId,更新最终的状态。
这样算起来10w条短信,最终发送完毕要更新30w次数据,而且这其中有两个要注意的是:1, 三个步骤必须按照顺序执行,否则where语句中需要的条件,数据库里根本还没有插入。2, 10w条瞬间发送,大量的写和回调在短时间内就要操作,要保证不会丢失。
针对这个情况我们采用队列的方式来处理,利用RabbitMQ来缓存所有的数据库操作。RabbitMQ即可以缓存大量的对象不会丢失,而且先进先出,保证了这三步执行的顺序。队列中缓存的对象封装成SmsDto这样一个类,除了必要的字段后,另外增加了一个sqlAction字段,在对象从队列中pop出来后,判断是进行第几步的数据库操作。
public static final int UPDATE_SEQID_STATE_BY_SMSID = 100;
public static final int UPDATE_MSGID_BY_SEQID = 101;
public static final int UPDATE_STATE_BY_MSGID = 102;
这样在发送快速执行的时候,不用担心数据库短时间内的大量写,可以通过队列pop的速度来控制写的速度。
其中系统优化的一个地方是队列pop写,开始的时候是每pop一个smsDto,根据sqlAction就执行一次数据库的操作,这样发现写的速度太慢,产品要求客户可以实时在网页上看到发送的统计结果,而我们的发送回调经常要写2,3个小时才能从队列里消费完。针对这个情况,就是要批量写数据库,然而又必须严格按照顺序来执行,所以就做了个3个List,根据sqlAction分别将smsDto放入这个三个不同的list里,在一分钟的间隔里,按照顺序来批量保存在3个List。这样保存的效率大大提高,基本上每条短信都可以实时从网页上看到发送状态。
/**
* 批量处理数据库操作
* 必须按照顺序执行
*
*/
private void batchSave(){
if (!step1List.isEmpty()){
smsService.save(step1List);
}
if (!step2List.isEmpty()){
smsService.save(step2List);
}
if (!step3List.isEmpty()){
smsService.save(step3List);
}
step1List.clear();
step2List.clear();
step3List.clear();
}
以上是socket方式发送短信的处理方式。http协议的方式一般是通过在网关设置一个自己系统的回调地址来接收发送报告和短信上行的。这就需要在自己系统实现两个接口来接收返回的信息。
http形式一般在url上可以批多个手机号来发送,类似于http://ip:port/send?content=abc&mobile=m1,m2,m3..我们使用的网关会在这个请求后返回一个唯一的码,类似于上述的msgId,唯一标示这一批短信的一个id。
在自己系统实现的短信状态回调接口类似于http://ip:port/report?msgId,mobile1,status,time;msgId,mobile2,status,time...这样就可以根据msgId和mobile来唯一确定数据库中的一条短信记录,来更新发送状态了。
短信网关的调用已经属于古老的成熟的技术了,大体情况也基本相同,一个成熟的网关都可以得到每条短信的发送结果,这些都从他提供的开发包,文档里来确定具体的实现方式。不同的可能就是各自在对大量状态更新方面的处理方式,这就需要结合各自的业务需要来实现了。