2023-03-11
原文作者:柒's Blog 原文地址:https://blog.52itstyle.vip/archives/1304/

202303111616015791.png

前言

前段时间搞了个SpringBoot开发案例之整合mail发送服务,也是基于目前各项目平台的邮件发送功能做一个抽离和整合。

问题

以发送方为例,比如网易的反垃圾邮件政策,过多或者频率过快的发送都会被判定为垃圾邮件,即使发再多的邮件也无济于事。当然如果是自建邮件服务器,也是要考虑发送服务的并发能力。

以接收方邮件为例,比如腾讯邮箱就有类似说明:如果内容涉嫌大量群发,并且被多数用户投诉为垃圾邮件,也就通过不了收件方的"安检",如下图:

202303111616022542.png

方案

为了解决以上实际场景中遇到的问题,使得其更像一个安全高效的邮件服务平台,我们尝试引入了任务队列对邮件发送进行流量削锋、间隔发送以及重复内容检测。

首先,我们先建一个队列MailQueue:

    /**
     * 邮件队列
     * 创建者 科帮网
     * 创建时间    2017年8月4日
     *
     */
    public class MailQueue {
         //队列大小
        static final int QUEUE_MAX_SIZE   = 1000;
    
        static BlockingQueue<Email> blockingQueue = new LinkedBlockingQueue<Email>(QUEUE_MAX_SIZE);
        
        /**
         * 私有的默认构造子,保证外界无法直接实例化
         */
        private MailQueue(){};
        /**
         * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
         * 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
         */
        private static class SingletonHolder{
            /**
             * 静态初始化器,由JVM来保证线程安全
             */
            private  static MailQueue queue = new MailQueue();
        }
        //单例队列
        public static MailQueue getMailQueue(){
            return SingletonHolder.queue;
        }
        //生产入队
        public  void  produce(Email mail) throws InterruptedException {
            blockingQueue.put(mail);
        }
        //消费出队
        public  Email consume() throws InterruptedException {
            return blockingQueue.take();
        }
        // 获取队列大小
        public int size() {
            return blockingQueue.size();
        }
    }

如文章开头图片所描述,这里我们还需要建一个消费线程池ConsumeMailQueue:

    /**
     * 消费队列
     * 创建者 科帮网
     * 创建时间    2017年8月4日
     */
    @Component
    public class ConsumeMailQueue {
        private static final Logger logger = LoggerFactory.getLogger(ConsumeMailQueue.class);
        @Autowired
        IMailService mailService;
        
        @PostConstruct
        public void startThread() {
            ExecutorService e = Executors.newFixedThreadPool(2);// 两个大小的固定线程池
            e.submit(new PollMail(mailService));
            e.submit(new PollMail(mailService));
        }
    
        class PollMail implements Runnable {
            IMailService mailService;
    
            public PollMail(IMailService mailService) {
                this.mailService = mailService;
            }
    
            @Override
            public void run() {
                while (true) {
                    try {
                        Email mail = MailQueue.getMailQueue().consume();
                        if (mail != null) {
                            logger.info("剩余邮件总数:{}",MailQueue.getMailQueue().size());
                            //可以设置延时 以及重复校验等等操作
                            mailService.send(mail);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        @PreDestroy
        public void stopThread() {
            logger.info("destroy");
        }
    }

改造service:
部分接口:

     /**
          * 队列
          * @Author  科帮网
          * @param mail
          * @throws Exception  void
          * @Date    2017年8月4日
          * 更新日志
          * 2017年8月4日  科帮网 首次创建
          *
          */
         public void sendQueue(Email mail) throws Exception;

部分实现:

        @Override
        public void sendQueue(Email mail) throws Exception {
            MailQueue.getMailQueue().produce(mail);
        }

队列说明

以上代码,大家可以看到我们有使用到了linkedblockingqueue来实现邮件发送队列。

LinkedBlockingQueue作为一个阻塞队列是线程安全的,同时具有先进先出等特性,是作为生产者消费者的首选。

LinkedBlockingQueue可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE。

其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。

202303111616027773.png

最后给大家补充一个非阻塞队列ConcurrentLinkedQueue,有兴趣的同学可以自行查阅资料。

分享总结

如果,你看到你写的代码是一坨屎的时候你就该去优化她了,好好爱护她,未来的你会为昨天的你而感到骄傲的。

其实,想表达的是,架构优化是无止境的,随着业务的增长以及平台的发展,我们会遇到各种各样的问题。

  • 邮件服务挂了,队列还没消费完怎么办?
  • 邮件服务挂了,我们是否应该做个高可用?
  • 邮件服务爆了,我们是否应该做个负载均衡?

以上问题,你又会怎么解决呢?下一篇为大家带来高可用的邮件服务平台。

项目案例:码云

阅读全文