关于"在用户设定的时间发送邮件"的功能设计

Submitted by Lizhe on Mon, 07/17/2017 - 17:23

这个问题其实还是Brian问的, 他真的去做那个 "拍卖系统" 了

这个拍卖系统有一个需求是  " 假设用户设定的拍卖开始时间是2017年1月1日中午12点, 那么系统需要在拍卖会开始之前半个小时给参与的bidder们发送提醒邮件"

这个功能看起来貌似很简单, 不过仔细想起来以前好像还真没碰到类似的

1. 发送邮件的job触发一次之后, 就会被移除 ( 不像springbatch或者crontab那种定时器触发, 要触发好多次)

2. job的数量不固定, 以前做的job都是程序员设定的, 不会由用户创建, 所以如果job数量特别大,可能会出问题

3. job的触发时间是用户设定的, 如果同一时间有大量job需要触发, 而且如果邮件发送特别慢的话,也可能会出问题

 

下面给出两个解决方案

  • 使用atd服务实现
  • 使用redis+rabbitmq写代码实现

总的来说各有利弊, 下面请看详细

 


第一种, 使用atd服务实现

安装atd服务
yum -y install at

启动atd服务
systemctl start atd
systemctl status atd

直接手动调用
[root@vagrant spool]# at 10:20 7/17/2017 
at> ls
at> <EOT>   ( 键盘 crtl+D )
job 3 at Mon Jul 17 10:20:00 2017
[root@vagrant spool]# at -l
3       Mon Jul 17 10:20:00 2017 a root


通过管道调用
touch testad |  at 10:22 7/17/2017


安装sendmail
yum install sendmail

Sendmail的配置文件由m4来生成,m4工具在sendmail-cf包中
yum install sendmail-cf

如果需要其他ip访问修改

vi /etc/mail/sendmail.mc
DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA')dnl

发邮件命令
echo "mail content"|mail -s test admin@test.com

我们要做的就是,把上面两个命令结合起来

echo "mail content at 10:45 7/17/2017 "|mail -s test admin@test.com |  at 10:45 7/17/2017

采用这种方式的话如果是简单的邮件内容和需求很简单而且几乎没有什么开发成本,但是如果需要监控邮件发送状态或者其他一些复杂业务
可能需要使用额外的脚本(例如使用java或者Python)来完成业务逻辑

这种方式还有一个问题是,如果等待发送的邮件特别多, 比如超过1万封邮件要在某个时间段发出去,而服务器根本处理不过来怎么办

实际上我也没想出什么"万全之策" 

不过如果你也没有好办法, 可以考虑我下面的模型

 


第二种写代码用redis+rabbitmq实现

 

使用一个简单的数据库(我建议你使用redis或类似产品以获得最大的查询性能及持久化支持)
每次客户端创建了一个定时发送邮件请求之后, 存储相关信息到数据库
例如
Tom 需要在下午1点发送一封指定内容的邮件给客户 t1,t2,t3,t4,t5 
Jerry 需要在下午1点半发送一封指定内容的邮件给客户 j1,j2,j3,j4,j5,j6

这里我使用json格式保存邮件内容信息
{"title":"Test Mail Subject","content":"this is a test content","receivers":["zhe.li@test.com","test@test.com"]}

在存入redis数据库时, 该数据会按照zset (有序集合) 的类型存入, 而有序集合这个key, 我们使用Long型的TimeInMillis
在这个例子中,我会取得当前时间now, 然后设定邮件需要在2分钟之后被发送
例如 现在是2017年7月17日8:00, 例子中设定2分钟之后发送是8:02

Calendar now = Calendar.getInstance();
now.add(Calendar.MINUTE, 2);
Long after2mins = now.getTimeInMillis();

然后使用camel (或者spring integration) 来轮询这个数据库 ( 例如每分钟查询一次 ), 以获取当前时间1分钟之内需要发送的邮件
当时间来到8:02时, camel会得到之前设定的8:02需要发送的数据
由于redis的遍历速度非常快, 所以即使在查询间隔中有大量数据涌入也不用怕

因为假设了是大量的邮件等待发送, 所以这里不建议你直接对数据进行操作,而是通过camel的processor直接将数据库的数据整理,
然后发给rabbitmq, rabbitmq会作为一个缓冲存在, 在转发给mq之后, 删除redis中的数据

例如我将 json数据 {"title":"Test Mail Subject","content":"this is a test content","receivers":["zhe.li@test.com","test@test.com"]} 存入数据库

camel遍历出这条记录之后会向rabbitmq发送两条记录(拆分成两个邮件)
Producer Send +'{"title":"Test Mail Subject","content":"this is a test content","receiver":"zhe.li@test.com"}'
Producer Send +'{"title":"Test Mail Subject","content":"this is a test content","receiver":"test@test.com"}'

注意 redis 对于范围查询的api
获得key=schmails 下的一个范围的集合
Set<String> results = jedis.zrangeByScore("schmails", before1mins.getTimeInMillis(), now.getTimeInMillis());
删除key=schmails 下的一个范围的集合
jedis.zremrangeByScore("schmails", before1mins.getTimeInMillis(), now.getTimeInMillis());


然后接着会有一个(或多个)camel processor 每30秒就来轮询这个mq, 一得到消息, 直接发送邮件, 这个解决方案的好处在于
由于发送邮件的业务逻辑中,往往网络开销比较大,服务器在发送邮件时可能会遭遇性能瓶颈, 但是我们这个方案因为采用了 生产者+消费者 模式, 所以你可以很容易的构建多个 "邮件发送服务器"构成一个发送集群来作为消费者


假设有3台server不停的轮询从mq里取得消息,然后一起发送邮件,性能和扩展性可以得到大大改善


431

存入mq时

 

434

可以看到mq中的两条消息

433

439

在mq中的消息被消费掉之后 ( 假设发送了邮件 ), 可以看到mq已经被清空

438

435

 

示例代码可以到

https://github.com/zl86790/ScheduledMailSender.git

下载