• 【案例实战】高性能SpringBoot整合短线验证码发送(池化思想+异步结合)


    1.需求背景
    • 海量用户下,高性能的发送手机短信验证码。
    • 为什么要用线程池+异步的方式去进行短信验证码的发送呢?
      • 如果是同步发送+RestTemplate未池化最大几百的吞吐量
      • 错误Caused by: java.io.IOException: Broken pipe
        • 服务端向前端socket连接管道写返回数据时 链接(pipe)却断开了
        • 从应用角度分析,这是因为客户端等待返回超时了,主动断开了与服务端链接
        • 连接数设置太小,并发量增加后,造成大量请求排队等待
        • 网络延迟,是否有丢包
        • 内存是否足够多支持对应的并发量
    2.第三方短信验证码平台接入
    • 阿里云市场:https://market.console.aliyun.com/imageconsole/index.htm
    • 这块只是给大家做个案例演示,公司的项目不一定采用云市场的短信接入,但是思路都是大同小异。

    (1)进入链接登入阿里云。

    在这里插入图片描述

    (2)大家可以根据自己的需求去进行购买,一般做测试的话就选3元的就可以了。

    • 产品链接:https://market.aliyun.com/products/57000002/cmapi00046920.html?spm=5176.2020520132.101.1.6646721898vIVt#sku=yuncode4092000001

    在这里插入图片描述

    (3)购买之后就会又对应的产品密钥。

    AppKey:204073759     
    
    AppSecret:LpKyOMk7krrJgU845P7UraJNs3wGtDN7
    
    AppCode:59712f17a3434de8b53b03df9bffe7e4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    3.SpringBoot项目搭建

    (1)新建Maven项目

    在这里插入图片描述
    在这里插入图片描述

    (2)引入Maven依赖

        
    	<parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.6.7version>
            <relativePath/> 
        parent> 
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    (3)创建主类

    @SpringBootApplication
    public class SendApplication {
        public static void main(String[] args) {
            SpringApplication.run(SendApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    (4)创建yaml,application.yml

    server:
      port: 8011
    spring:
      application:
        name: send-server
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (5)启动验证

    在这里插入图片描述

    4.开发短信验证码发送

    (1)加入Maven依赖

            <dependency>
                <groupId>org.apache.httpcomponentsgroupId>
                <artifactId>httpclientartifactId>
                <version>4.5.13version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)配置RestTemplate连接池

    什么是RestTemplate?
    
    * RestTemplate是Spring提供的用于访问Rest服务的客户端
    * 底层通过使用java.net包下的实现创建HTTP 请求
    * 通过使用ClientHttpRequestFactory指定不同的HTTP请求方式,主要提供了两种实现方式
      * SimpleClientHttpRequestFactory(默认)
        * 底层使用J2SE提供的方式,既java.net包提供的方式,创建底层的Http请求连接
        * 主要createRequest 方法( 断点调试),每次都会创建一个新的连接,每次都创建连接会造成极大的资源浪费,而且若连接不能及时释放,会因为无法建立新的连接导致后面的请求阻塞
      * HttpComponentsClientHttpRequestFactory
        * 底层使用HttpClient访问远程的Http服务
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Spring的restTemplate是对httpclient进行了封装, 而httpclient是支持池化机制

    /**
     * @description RestTemplate配置类
     * @author lixiang
     */
    @Configuration
    public class RestTemplateConfig {
    
        @Bean
        public RestTemplate restTemplate(ClientHttpRequestFactory requestFactory){
            return new RestTemplate(requestFactory);
        }
    
        @Bean
        public ClientHttpRequestFactory httpRequestFactory() {
            return new HttpComponentsClientHttpRequestFactory(httpClient());
        }
    
        @Bean
        public HttpClient httpClient(){
            Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", SSLConnectionSocketFactory.getSocketFactory())
                    .build();
    
            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
    
            //设置整个连接池最大连接数
            connectionManager.setMaxTotal(500);
            //设置每个主机的最大并发数
            connectionManager.setDefaultMaxPerRoute(200);
            RequestConfig requestConfig = RequestConfig.custom()
                    //设置返回数据的超时时间
                    .setSocketTimeout(20000)
                    //设置连接服务器的超时时间
                    .setConnectTimeout(10000)
                    //设置从连接池中获取连接的超时时间
                    .setConnectionRequestTimeout(1000)
                    .build();
    
            return HttpClientBuilder.create()
                    .setDefaultRequestConfig(requestConfig)
                    .setConnectionManager(connectionManager)
                    .build();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    (3)Async+ThreadPoolTaskExecutor配置自定义线程池

    @Configuration
    public class ThreadPoolTaskConfig {
    
        @Bean("threadPoolTaskExecutor")
        public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
    
            ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
            //线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
            //如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
            threadPoolTaskExecutor.setCorePoolSize(4);
    
            //最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
            //当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
            threadPoolTaskExecutor.setMaxPoolSize(8);
    
            //缓存队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行
            threadPoolTaskExecutor.setQueueCapacity(124);
    
            //当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
            //允许线程空闲时间60秒,当maxPoolSize的线程在空闲时间到达的时候销毁
            //如果allowCoreThreadTimeout=true,则会直到线程数量=0
            threadPoolTaskExecutor.setKeepAliveSeconds(30);
    
            //spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。
            //jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
            threadPoolTaskExecutor.setThreadNamePrefix("common-thread-");
            threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
    
            // rejection-policy:当pool已经达到max size的时候,如何处理新任务
            // CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
            //AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
            //DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
            //DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
    
            threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            threadPoolTaskExecutor.initialize();
            return threadPoolTaskExecutor;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    (4)配置yml

    # sms短信配置
    sms:
      #这个appcode就是在下单之后,商家会提供一个appcode
      app-code: 59712f17a3434de8b53b03df9bffe7e4
      #模板ID,这块需要自己去申请自定义的短信模板内容,我这块用的是测试的模板
      template-id: M105EABDEC
      #短信发送URL
      send-url: https://jmsms.market.alicloudapi.com/sms/send?mobile=%s&templateId=%s&value=%s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 这块告诉大家怎末去获取测试的模板ID。注意测试模板ID会不定时更新,发现测试模板ID不好用了,及时去云市场产看。
    • 这个url的调用文档写的也是很详细了。
    • 链接:https://market.aliyun.com/products/57000002/cmapi00046920.html?spm=5176.2020520132.101.1.66467218VlNfII#sku=yuncode4092000002

    在这里插入图片描述

    (5)编写短信配置类

    @ConfigurationProperties(prefix = "sms")
    @Configuration
    @Data
    public class SmsConfig {
    
        /**
         * 短信模板ID
         */
        private String templateId;
    	
        /**
         * 短信app-code
         */
        private String appCode;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    (6)编写短信发送组件

    /**
     * 短信发送服务
     * @author lixiang
     */
    @Component
    @Slf4j
    public class SmsComponent {
    
        @Value("${sms.send-url}")
        private String sendCodeUrl;
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Autowired
        private SmsConfig smsConfig;
        
        @Async("threadPoolTaskExecutor")
        public void send(String to, String templateId, String value) {
            String url = String.format(sendCodeUrl, to, templateId, value);
            HttpHeaders headers = new HttpHeaders();
            headers.set("Authorization","APPCODE "+smsConfig.getAppCode());
    
            HttpEntity<String> entity = new HttpEntity<>(headers);
    
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
            log.info("url:{},body:{}", url, response.getBody());
            if (!response.getStatusCode().is2xxSuccessful()) {
                log.error("短信发送失败,value:{},响应:{}", value,response.getBody());
            }else{
                log.info("短信发送成功,value:{},响应:{}",value, response.getBody());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    (7)编写Controller测试

    @RestController
    @RequestMapping("/notify")
    public class SendController {
    
        @Autowired
        private SmsComponent smsComponent;
    
        @Value("{sms.template-id}")
        private String templateId;
    
        @GetMapping("send_code")
        public String sendCode(@RequestParam("phone") String phone){
    		//定义发送的验证码,公司的业务可以采用随机生成的4位或者6位数字
            String code = "567249";
            //发送短信验证码
            smsComponent.send(phone,templateId,code);
            return "SUCCESS";
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    ok,至此SpringBoot整合短信验证码发送已经完成。下面我们来测试一下性能。

    以下是同步发送+池化RestTemplate的压测报告,如果异步发送性能还会更高

    在这里插入图片描述

  • 相关阅读:
    路由器配置DMZ主机映射
    【VUE】ArcoDesign之自定义主题样式和命名空间
    导数求函数的单调性与极值
    中小型企业网络实战topo
    JavaStringBuffer与StringBuilder
    Java空指针异常的正确理解
    SpringBoot配置文件 yaml的使用
    【继承和多态】
    Apache Echarts介绍与入门
    供应试剂mPEG-Methacrylate,mPEG-MAC,CAS:26915-72-0
  • 原文地址:https://blog.csdn.net/weixin_47533244/article/details/127819480