一、业务背景
业务场景是基于类似华为小米等手机厂商的一项基本功能:消息推送。
消息推送的核心目标就是通过推送服务,将开发者指定的消息内容推送到指定的用户手机上进行显示,完成一些运营、用户提醒等目标。
简化的消息推送链路如下:
由于推送服务需要和设备之间维持长链接,所以需要一个唯一标识来标识长链接通道,后续推送时候就可以根据这个唯一标识找到通道进行下发消息推送。暂且将该标识标为conId。
推送服务连接客户端和开发者,开发者在推送的时候,为了管理方便,需要对每个设备的conId进行标记,方便后续根据标记进行推送,比如,给某个设备的conId起别名、将APP上对美食感兴趣的用户conId加入某个topic主题等,后续就可以直接指定别名或者主题进行推送。
因此,在推送服务端,需要保存conId和topic的订阅关系,通常是一个topic对应多个conId。
更复杂的场景,一个用户设备可能会订阅不同的主题,比如某个设备conId订阅了美食和旅游这两个标签,在进行推送的时候,可能指定两个或者多个主题进行组合推送,其中主题下面的conId元素可能就会重复,此时需要根据用户选择进行元素的交集/并集/差集处理。
比如:
topic1中的元素:1、2
topic2中的元素:2、3
求交集:2
求并集:1、2、3
topic1对topic2的差集:1
topic2对topic1的差集:3
备注:差集结果和主题的顺序有关系。
因此,该业务场景中主要的逻辑有两个:
- topic和conId的映射关系存储
- 多个topic组合的结果运算
二、技术难点
由于推送服务要支撑在该平台上所有上架的应用APP的消息推送功能,因此,数据量会非常庞大,假设有100个APP在平台上上架,每个APP平均有100W用户,平均每个APP上有20个topic,每个topic有50W用户,则topic和conId总的订阅关系数据量=100 ** 20 * 500000 = 2亿。*
而且这还是按照最少的数量进行估计。
因此对应的技术难点有两个:
- 数据量大的存储
- 大数据量下的交集并集差集运算
三、方案设计
由于推送是个瞬发的过程,因此需要保证运算可以实时进行,调研了一下支持存储+实时运算的存储有:
- redis
-
- 优点:实时计算性能高
- 缺点:存储成本高,且有数据丢失风险
- MySQL
-
- 优点:使用简单
- 缺点:大数据量存储下查询性能低下
- 一些实时计算的高性能数据库,如clickhouse
-
- 优点:存储可以通过分布式存储来实现海量数据存储,并且大数据量下可以做到实时查询
一开始使用的是Redis的set集合来存储topic和conId的订阅关系,并且set集合自带了交集并集差集的查询,用起来比较方便,但是后续在Redis cluster集群下发现问题,不同topic下的数据可能分布在不同的hash slot里面,cluster集群不支持跨slot的set集合进行运算。会报错:
CROSSSLOT Keys in request don't hash to the same slot. channel: [id: 0xed09a912, L:/172.166.1.114:50189 - R:10.164.113.113/10.164.113.113:6379] command: (SINTER), promise: RedissonPromise [promise=ImmediateEventExecutor$ImmediatePromise@14d48ac8(incomplete)], params: [[99, 58, 115, 116, 97, 114, 45, 99, 108, 111, ...], [99, 58, 115, 116, 97, 114, 45, 99, 108, 111, ...]]; nested exception is org.redisson.client.RedisException: CROSSSLOT Keys in request don't hash to the same slot. channel: [id: 0xed09a912, L:/172.166.1.114:50189 - R:10.164.113.113/10.164.113.113:6379] command: (SINTER), promise: RedissonPromise [promise=ImmediateEventExecutor$ImmediatePromise@14d48ac8(incomplete)], params: [[99, 58, 115, 116, 97, 114, 45, 99, 108, 111, ...], [99, 58, 115, 116, 97, 114, 45, 99, 108, 111, ...]]
解决办法如下:
- 使用redis cluster提供的key命名规则,如:XXX{XXX}XX,就可以根据{}内的内容来进行hash,将所有的topic数据都放入同一个分片中。
缺点是:会导致单个分片存储和访问、计算压力巨大,并且无法扩容。\
- 读取所有需要运算的topic中的数据到业务服务器中来进行运算。缺点是:需要自行实现运算代码,并且如果topic数量太多的话,会导致业务服务器占用内存巨大。
- 额外提供一台专门用来计算的redis服务器,需要运算时,将各个topic的数据先写入该台redis,然后做运算,返回结果后将数据删除。
优点是:存储和计算分离,。
缺点:需要额外的redis资源,如果单个topic下面的regid数量太大,一次性加载到业务服务器内存中可能会导致内存爆炸。
总结下来,Redis实现的话一方面数据量大需要很高的存储成本,另一方面cluster集群模式下不支持交集并集差集运算。
因此,最终采用了方案三:实时计算的数据库,不过由于采购原因,使用的是华为云付费的DWS数仓数据库,主要特点是:
- 高性能查询:分布式架构,万亿规模数据量查询可以达到秒级响应
- 易扩展,可以按需扩展,扩容过程可不中断业务,扩容后性能提升
- 易用性:支持JDBC连接和访问DWS,兼容大部分SQL语句
- 支持SQL写入和删除数据
DWS数据仓库架构介绍:support.huaweicloud.com/productdesc…
整体的业务流程:
- 开发者将regid与topic主题绑定后,需要将数据入MYSQL、Redis、DWS数仓:
表结构:
- 开发者发起推送后,判断是否有涉及多个topic的交集并集差集查询,如果有,则根据连接类型写SQL进行交并差的查询。
查询SQL:
表里数据示例:
- 交集:SQL关键字:INTERSECT
SELECT p1.reg_id from push_topic_regid_relation p1 where p1.topic_name = 'tp1'
INTERSECT
SELECT p2.reg_id from push_topic_regid_relation p2 where p2.topic_name = 'tp2'
查询结果:
- 并集:SQL关键字:有重复记录:UNION ALL,没有重复记录:UNION
SELECT reg_id from push_topic_regid_relation where topic_name = 'tp1'
UNION
SELECT reg_id from push_topic_regid_relation where topic_name = 'tp2'
查询结果:
- 差集:SQL关键字:EXCEPT
SELECT p1.reg_id from push_topic_regid_relation p1 where p1.topic_name = 'tp1'
EXCEPT
SELECT p2.reg_id from push_topic_regid_relation p2 where p2.topic_name = 'tp2'
查询结果:
四、总结
技术选型需要综合考虑存储、性能、成本各个方面,在各个方面取得一个平衡。
文章中的场景是一个JAVA结合大数据实现功能的案例,有时候应用JAVA技术成本太高的话,有必要考虑大数据的技术。
实时数仓技术适合数据量大,查询很多,新增修改很少,但是查询比较简单的场景,如果数据量很大,并且查询比较复杂,可能查询性能就会很慢,这时候就需要考虑是否真正需要做到实时。