1.需求背景
- 海量用户下,高性能的发送手机短信验证码。
- 为什么要用线程池+异步的方式去进行短信验证码的发送呢?
- 如果是同步发送+RestTemplate未池化最大几百的吞吐量
错误Caused by: java.io.IOException: Broken pipe
- 服务端向前端socket连接管道写返回数据时 链接(pipe)却断开了
- 从应用角度分析,这是因为客户端等待返回超时了,主动断开了与服务端链接
- 连接数设置太小,并发量增加后,造成大量请求排队等待
- 网络延迟,是否有丢包
- 内存是否足够多支持对应的并发量
2.第三方短信验证码平台接入
- 这块只是给大家做个案例演示,公司的项目不一定采用云市场的短信接入,但是思路都是大同小异。
(1)进入链接登入阿里云。
(2)大家可以根据自己的需求去进行购买,一般做测试的话就选3元的就可以了。
(3)购买之后就会又对应的产品密钥。
AppKey:204073759 AppSecret:LpKyOMk7krrJgU845P7UraJNs3wGtDN7 AppCode:59712f17a3434de8b53b03df9bffe7e4
3.SpringBoot项目搭建
(1)新建Maven项目
(2)引入Maven依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.7</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
(3)创建主类
@SpringBootApplication public class SendApplication { public static void main(String[] args) { SpringApplication.run(SendApplication.class, args); } }
(4)创建yaml,application.yml
server: port: 8011 spring: application: name: send-server
(5)启动验证
4.开发短信验证码发送
(1)加入Maven依赖
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>
(2)配置RestTemplate连接池
什么是RestTemplate? * RestTemplate是Spring提供的用于访问Rest服务的客户端 * 底层通过使用java.net包下的实现创建HTTP 请求 * 通过使用ClientHttpRequestFactory指定不同的HTTP请求方式,主要提供了两种实现方式 * SimpleClientHttpRequestFactory(默认) * 底层使用J2SE提供的方式,既java.net包提供的方式,创建底层的Http请求连接 * 主要createRequest 方法( 断点调试),每次都会创建一个新的连接,每次都创建连接会造成极大的资源浪费,而且若连接不能及时释放,会因为无法建立新的连接导致后面的请求阻塞 * HttpComponentsClientHttpRequestFactory * 底层使用HttpClient访问远程的Http服务
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(); } }
(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("小滴课堂Spring自带Async前缀:"); threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true); // rejection-policy:当pool已经达到max size的时候,如何处理新任务 // CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行 //AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。 //DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常 //DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列 threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); threadPoolTaskExecutor.initialize(); return threadPoolTaskExecutor; } }
(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
- 这块告诉大家怎末去获取测试的模板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; }
(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()); } } }
(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"; } }
ok,至此SpringBoot整合短信验证码发送已经完成。下面我们来测试一下性能。
以下是同步发送+池化RestTemplate的压测报告,如果异步发送性能还会更高