来看这样一个问题:
某账号系统的账号都在unsigned int内, 也就是0-42亿左右。 在这42亿账号中, 有大约1亿账号是相对非常活跃的用户, 用户和用户之间可以建立好友关系(类似于微信那样的好友关系)。 现在要设计一个定时赠言系统, 比如: 今天是2017年3月25日, 那么用户A可以给他的好友B送定时赠言(精确到未来某天, 不包括今天), 预期未来这天到达。而且任何用户都可以给他的多个好友多次送定时赠言。试给出合理的大致设计方案
我们一起来逐步看下, 这里肯定要把定时任务先落地到存储中, 到了设定的定时日期, 读出来, 并发出去。 那存储的数据结构该怎样组织呢? 这是本问题的核心所在。
方案一:
对于每一个定时任务, 用四元组把它存起来, 即{发送者账号, 接受者账号, 定时时间, 赠言}, 对于n个赠言操作, 就有了n个四元组, 然后把这n个四元组存储起来。 然后每天都读取任务, 读取到n个四元组任务后, 根据定时时间筛选出该发送的, 发送出去。
举个例子, 在2017年3月25日, 用户123给好友456送了定时赠言“”“I miss you”, 期望2017年3月28日到, 那么对应的四元组是{123, 456, 20170328, "I miss you"}, 然后存储起来, 于是乎, 在2017年3月26日, 遍历读取存储中的众多定时任务, 发现了{123, 456, 20170328, "I miss you"}, 但时间不对,所以暂时不发。 等2017年3月27日再读取, 发现时间又不对, 所以暂时不发。 等2017年3月28日再读取, 发现时间对了, 就立即发出消息。
我们来审视下这个方案, 很容易发现, 这是个相当低级的设计, 2017年3月26日和2017年3月27日做了两次无用的尝试啊。 从整体来看, 这种无用次数的数量难以想象。问题显而易见, 我们没有按照时间来存储这些定时任务。 于是继续改造。
方案二:
以设定的定时时间为key, 将所有定时在这一天的任务, 落地到同一个存储区。 按照上面的例子, 简单来说就是, 以20170328为key, 于是上述赠言操作就对应一个三元组,{123, 456, "I miss you"}, 读取的时候, 按天读取任务, 然后发出。 这种方案就好多了。 但还是有问题: 三元组作为value, 有n个留言操作, 就是n个三元组的拼接, 太庞大了, 不满足系统的key value存储要求。 于是继续优化。
方案三:
以{20170328, 123}做key, 用{456, "I miss you"}做value, 这样, 通过增加key的个数,来减少value的长度。 但问题在于, 读取的时候怎么去索引呢? 难道要遍历{20170328, 0} --- {20170328, 42亿} ? 我们要考虑到, 只有少数活跃用户啊, 这种42亿的全遍历肯定是不行的。
这里问题的本质是什么? 本质就是漫无目的暴力地去遍历key, 何不把有实际操作的key存下来呢?
方案四: 以20170328为一级key, 将有实际操作的用户作为value, 于是形成了这样的数据结构:20170328--->{123, 231, 101...}, 其中123, 231, 101都是有实际赠言操作的用户。 然后以{20170328, 123}位作为二级key来存放 {456, "I miss you"}. 到了2017年3月28日这边, 读取操作就很顺畅了, 先读取一级value {20170328, 123}, 然后以第一级value为key读取二级value {456, "I miss you"}.. 但发现还有个问题, 二级value的值还是太大(m个二元组的拼接, 很大)。继续优化。
方案五: 根据发送者尾号(比如取尾4位数)对key进行打散, 于是用户123和用户9870123的一级key就是一样的了, 都是{20170328,0123}. 该怎么读取呢? 到了2017年3月28日那边, 遍历{20170328, 0000}---{20170328, 9999}这一万个key(几乎可以肯定的是, 不会存在空遍历的情况), 于是查到123这个用户有赠言操作,于是继续用{20170328, 0123, 123}来获取到二级value {456, "I miss you"}, 然后把消息发出。
好了, 对比下方案一和方案五, 就知道优劣了。 随便说两点:
1. 方案一没法用多进程跑读任务, 慢得蛋疼, 呵呵哒。
2. 方案一没法在紧急情况时取消任务, 都杂糅在一起了, 取消很蛋疼的。
当然, 方案的优化是无止境的, 目前来看, 方案五跑得很顺畅。
蛮力要不得, 任何事情, 都得讲点策略