引入
BLER目前有两个业务应用到了邮件发送的功能:
- 新用户注册时的邮箱验证
- 企业处理简历时自动向应聘者发送的邮件
例如,在用户注册模块,使用了下面的代码简单地发送了一个邮件:
1 |
|
其中的邮箱发送工具类:
1 | /** |
然而,这种方式在多个线程并发执行邮件发送功能时,可能存在许多问题:
- 性能瓶颈: SMTP服务器通常有限制同一时间内来自同一IP的并发连接数或者发送速率。大量并发线程可能超出SMTP服务器允许的并发处理能力,导致部分连接被拒绝或延迟响应。
- 网络带宽限制: 大量并发发送邮件会消耗大量的网络带宽,尤其是在高并发场景下,可能导致网络拥塞,降低整体系统性能。
- 邮箱服务商反垃圾邮件策略: 部分邮件服务商会对短时间内从同一IP地址发送大量邮件的行为进行监控,并可能触发反垃圾邮件机制,从而将您的服务器IP标记为可疑或黑名单,影响正常邮件送达。
为了解决上面的问题,可以使用消息队列中间件RabbitMQ实现异步的处理,来解决上述的问题。
RabbitMQ简介
RabbitMQ是最受欢迎的开源消息中间件之一,在全球范围内被广泛应用。RabbitMQ是轻量级且易于部署的,能支持多种消息协议。RabbitMQ可以部署在分布式系统中,以满足大规模、高可用的要求。
RabbitMQ的5种消息模式
简单模式
简单模式包含一个生产者、一个消费者和一个队列。生产者向队列里发送消息,消费者从队列中获取消息并消费。
在我们目前讨论的场景(邮件异步发送)中,就是应用了这种工作模式。
代码实现:
1.创建配置类:
1 |
|
2.创建生产者
1 | public class SimpleSender { |
3.创建消费者
1 | //指定其绑定的队列 |
4.在需要执行功能的地方,直接调用生产者的方法
1 |
|
工作模式
工作模式是指向多个互相竞争的消费者发送消息的模式,它包含一个生产者、多消费者和一个队列。两个消费者同时绑定到一个队列上去,当消费者获取消息处理耗时任务时,空闲的消费者从队列中获取并消费消息。
代码实现:
1.创建配置类
1 |
|
2.定义生产者,生产者通过send方法
向队列work.hello
中发送消息,消息中包含一定数量的.
号;
1 | public class WorkSender { |
3.定义消费者,两个消费者从队列work.hello
中获取消息,名称分别为instance 1
和instance 2
,消息中包含.
号越多,耗时越长;
1 |
|
4.调用生产者方法
1 |
|
发布/订阅模式
发布/订阅模式是指同时向多个消费者发送消息的模式(类似广播的形式),它包含一个生产者、多消费者、多队列和一个交换机。消费者同时绑定到不同的队列上去,队列绑定到交换机上去,生产者通过发送消息到交换机,所有消费者接收并消费消息。
代码实现
1.创建配置类
1 |
|
2.创建生产者,注意这次发送的对象是交换机而不是队列
1 | public class FanoutSender { |
3.定义消费者
1 | public class FanoutReceiver { |
4.调用生产者方法,不再赘述
路由模式
路由模式是可以根据路由键
选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键
绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键
转发到不同队列,队列绑定的消费者接收并消费消息。
代码实现
1.创建配置类
1 |
|
2.定义生产者,生产者通过send方法
向交换机exchange.direct
中发送消息,发送时使用不同的路由键
,根据路由键
会被转发到不同的队列;
1 | public class DirectSender { |
3.定义消费者,消费者从自己绑定的匿名队列中获取消息,由于该消费者可以从两个队列中获取并消费消息,可以看做两个消费者,名称分别为instance 1
和instance 2
;
1 | public class DirectReceiver { |
4.调用生产者方法
通配符模式
通配符模式是可以根据路由键匹配规则
选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键匹配规则
绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键匹配规则
转发到不同队列,队列绑定的消费者接收并消费消息。
与发布/订阅模式相比,通配符模式更加灵活自由了,其区别仅在于配置类中绑定方法的路由键设置,可以使用通配符。
代码实现
配置文件如下:
1 |
|
RabbitMQ的应用
1.配置依赖
- 在
pom.xml
中添加AMQP相关依赖;
1 | <!--Spring AMQP相关依赖--> |
- 修改
application.yml
文件,在spring节点下添加RabbitMQ相关配置。
1 | spring: |
2.实现简单模式的消息队列
定义配置类
1 |
|
定义生产者
1 |
|
定义消费者
在这里实际进行邮件发送操作
1 |
|
修改Service中的代码
1 |
|
总结
通过上面实现的异步邮件发送系统,避免了多线程并发情况下邮件发送功能可能出现的问题。
不过,这样的设计会导致并发的压力给到了下面这一行代码:
1 | template.convertAndSend(queueName,EmailTask(user, identifyCode)); |
这会不会导致类似的问题呢?
经过查阅资料,得到了下面的答案:
消息队列的并发控制: 当多个线程同时调用
convertAndSend()
方法将邮件任务放入RabbitMQ队列时,RabbitMQ服务器会确保每个消息的入队操作是线程安全的,不会因为并发而丢失或破坏消息。生产者端的并发处理: 在生产者(即您的服务)这一侧,即使有多个线程并发调用
convertAndSend()
,它们实际上是并行地将任务推送到同一个队列中,而不是真正并发执行邮件发送动作。因此,在生产者端并没有实际意义上的邮件并发发送问题。也就是说,对于请求提交和消息入队列的操作,RabbitMQ是允许并行进行的,而实际的插入队列操作,则是串行地按顺序逐一进行的。这保证了线程安全性。
消费者端的任务处理: 并发问题更多可能出现在消费者端。然而,RabbitMQ通过其内部机制(如prefetch count设置等)可以实现对消费者的并发消费进行控制,例如确保同一时刻只有一个邮件任务被单个消费者消费和处理。这样就避免了邮件的实际并发发送,同时也实现了负载均衡。
所以,尽管convertAndSend()
方法在生产者端可能存在并发调用,但在使用RabbitMQ工作队列模式下,我们通过队列的机制有效管理和控制了任务的并发处理,并且避免了直接并发发送邮件可能导致的各种问题。