问题描述
以前曾经思考过类似的问题,比如腾讯公司,每逢生日,都会给用户发一封邮件。按QQ的用户规模,假设全国每人一个QQ,那么用户表有14亿条。如何给当天生日的人发送祝福邮件?这问题其实挺有意思的,很多可以思考和优化的点,于是我也想了一个初步方案。
一个系统设
系统总人数为Total。
假设,用户用户生日是分布均匀的,也就是说,每天的生日人数=1/365 * Total = 1/365 * 14亿 ~= 329 万,也就是当天需要发329万封邮件。那这个功能,就变成设计一个如何发329万封邮件的任务了。
系统设计可以抽象为一个任务队列,可以建立一个生产者-消费者的模型。
实现步骤
假设都存在MySQL的一张user表中,(生产环境上14亿条的数据表,肯定会做分库分表,暂时不考虑这个)。先简化业务模型。
实现步骤
1.遍历整张数据表
2.筛选目标用户
3.发送邮件
1. 如何遍历数据表
问题变成了:如何快速遍历整张表。
方案一:
1. 单线程遍历user表,每次取n=1000条:select * from a where id > lastId order by id limit 1000;(这一步是可以优化)
- 14亿条数据,每次取1000条,需要遍历的次数t1=1400 / 1000 = 140万次
- 14亿条数据,每次取10000条,需要遍历的次数t1=1400 / 10000 = 14万次
方案一似乎有点慢,因此可以考虑多线程,分段遍历,优化一下。
方案二:
总数14亿数据,可以考虑交给14个线程分段遍历,每个工作线程遍历1亿条数据。如此,理论上可以降低一个数量级
总数14亿数据,可以考虑交给14个线程分段遍历,每个工作线程遍历1亿条数据。如此,理论上可以降低一个数量级
每个线程的工作量
– 1亿条数据,每次取1000条,需要遍历的次数t2=10000w / 1000 = 10万次
– 1亿条数据,每次取10000条,需要遍历的次数t2=10000w / 10000 = 1万次
这样看起来,好像快了一点。
假设数据量为t,每个worker负责遍历的数据量为 s
归纳:worker数量 = 总数/每个worker负责遍历的数据量 = t/s
需要考虑的问题,MYSQL一次取1000条,按以往的经验,能撑得住的。一万条呢?一般来说,数据库服务器都能撑住,再不行可以读读写分离出来后的从库实例。
2. 帅选目标用户
遍历选出user.birthday.equals(today)的数据,目标结果为targetUserList列表.
3.发送邮件
mailService.send(userInfo,emailTemplateId);
mailService.batchSend(userInfoList,emailTemplateId);
发送邮件除了同步调用之外,可以做成异步调用。比如说,可以发消息,到kafka消息队列上面。然后再分批消费消息,而且kafka支持大量积压,不用担心消息堆积问题。此外,可以通过拓展服务器,多实例部署,来加快消费速度,这个拓展相对比较容易一些。
本文只是给一个初步的方案,肯定还有很多值得优化的地方,可以继续探讨和优化。
思考&优化
- 如果数据不是存在MySQL,那应该怎么做呢?(如果有MySQL从库,那么利用好从库)
不管是什么存储,都可以做成离线的,或者提前一天计算好。以前和大数据的同事,一起测试几百万的数据发送到MQ消息队列,不到半个小时就发完了,所以这方面可行性很高。 - 数据的读取和筛选,可以设计成多线程的,增加算力
- 如果系统要设计得灵活一点,kafka是一个关键的中间件
- 邮件发送接口,可以考虑支持批量发送 (合并操作)
- 数据分片读取(多线程),如何拆分,拆分粒度(结合限流)
- 接口是否需要限流 (邮件接口的API调用量配额)
- 是否需要业务去重
- 是否需要补偿计划(保证每个生日的人都收到祝贺邮件)
- 发送历史记录是否需要落地(birthdayTime,receiveUserId,templateId,content,operatorId,operateTime),可以支持异步落地到Apache Hive (sql-like db)
- 业务层面的考虑:这个全局发送邮件,视为一个任务。那么这个任务的实时性,需要多久必须完成。如果不需要5分钟、10分钟发完等短时间发送完毕;如果可以接受2-4个小时内发完(跟产品讨论),这就降低了系统设计的复杂度,给设计带来很多的灵活性。需要快速发送,那么水平拓展应用服务实例就可以了
(完)