最近开发了一个组件就是短信验证码发送功能,这个功能点首先支持了高并发大量请求的处理,和验证码防刷的知识点,细化共包含了以下3个技术点:
1、高并发异步请求,该工作主要使用到了@Async注解和ThreadPoolTaskExecutor自定义线程池,其主要思想是原请求方式即没有Async注解的方法被请求时,会被阻塞,需要等待该方法体完全被执行完才会返回json响应体,而加了@async后,不需要等待他执行完,直接返回一个请求值,而方法体的代码会被放到另一个线程中等待执行。另外可以自定义线程池ThreadPoolTaskExecutor,根据实际生产活动的机器调整线程池配置,即核心线程、阻塞队列、最大线程数的值;
其中关于@async线程有以下几个知识点:核心线程数、阻塞队列、最大线程数、拒绝策略、线程释放策略,其运作过程如下:
1)当请求进来时,会先判断当前请求加进来后,当前执行的线程数是否小于或等于核心线程数(这里的核心线程数指的是最小线程数、就是计算机空闲的时候也会保留的线程数量),如果仍小于或等于核心线程数,就会执行这个线程,否则就会被放到阻塞队列(默认是21亿)中;
2)当核心线程数满了,以及阻塞队列也满了,请求再进来的时候会判断是否小于最大线程数,如果仍小于或等于最大线程数,这个请求会直接被执行,否则这个请求就会被执行拒绝策略(拒绝策略有拒绝接收(接收报错)、接收但放弃(接收不报错)等);
3)当任务执行完了,逐渐小于核心线程或最大线程数了就会先判断阻塞队列中是否有需要处理的任务,有就会直接拿一条任务来执行,直到阻塞队列的任务逐个执行完成;
4)如果阻塞队列的都执行完了,空闲的线程就会根据回收策略回收时间逐个释放,直到小于核心线程数,避免资源浪费;
2、池化链接,该技术主要是使用了RestTemplate的连接池封装配置,避免了每次链接都需要3次握手和使用完毕须释放的问题,其原理是因为原来的RestTemplate底层使用的是java.net提供的方式,每次都需要创建一个TCP链接,然后都需要进行3次握手,每次使用完都需要释放这个链接,不然,后面无法建立新的连接导致后面的请求阻塞,这种池化方法性能至少提升10倍;
3、验证码缓存,用户在前端刷新的验证码会被缓存到redis中,并设置过期时间一般为60s,redis的key是???????????,value是验证码
4、短信验证码防刷,防止被耗光验证码。其代码逻辑是,先判断验证码是否一致,然后进入发送验证码的验证策略,其主要使用到redis数据库;
1)???????????+业务类型名称+手机号码获取是否发送过短信验证码;
2)没有则进入发送流程,获取value值,根据”_“截取时间戳,当前时间戳-value时间戳,判断是否大于10分钟,如果大于10分钟则发送验证码,如果小于就不发送验证码;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.config.SmsConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
@Slf4j
public class SmsComponent {
private static final String urlTemplate = "https://jmsms.market.alicloudapi.com/sms/send?mobile=%s&templateId=%s&value=%s";
@Autowired
private RestTemplate restTemplate;
@Autowired
private SmsConfig smsConfig;
@Async("threadPoolTaskExecutor")
public void send(String to, String templateId, String value) {
String url = String.format(urlTemplate, to, templateId, value);
HttpHeaders headers = new HttpHeaders();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.set("Authorization", "APPCODE " + smsConfig.getAppCode());
HttpEntity<String> entity= new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
// url=https://jmsms.market.alicloudapi.com/sms/send?mobile=13923273065&templateId=JM1000372&value=123456,body={"data":{"taskId":"JS6232241065583584"},"msg":"成功","success":true,"code":200,"taskNo":"655132717165003832312054"}
log.info("url={},body={}", url, response.getBody());
if (response.getStatusCode() == HttpStatus.OK) {
log.info("发送短信成功,响应信息:{}", response.getBody());
} else {
log.error("发送短信失败,响应信息:{}", response.getBody());
}
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
@Bean("threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
//线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
//如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
// executor.setAllowCoreThreadTimeOut();
executor.setCorePoolSize(16);
//缓存队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行
executor.setQueueCapacity(1024);
//最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
//当任务队列已满,且线程数=maxPoolSize时,线程池会拒绝处理任务而抛出异常
executor.setMaxPoolSize(64);
//当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
//允许线程空闲时间60秒,当maxPoolSize的线程在空闲时间到达的时候销毁
//如果allowCoreThreadTimeout=true,则会直到线程数量=0
executor.setKeepAliveSeconds(30);
//spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。
//jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
executor.setThreadNamePrefix("自定义线程池:");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
//AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
//DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
//DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
执行流程
1.CorePoolSize是否满足;
2.是Queue阻塞队列是否满;
3.才是MaxPoolSize是否满足;
4.走拒绝策略;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* 提供的一个RESTful风格的HTTP客户端,可以用来调用RESTful接口和处理HTTP请求响应。
*/
@Configuration
public class RestTemplateConfig {
@Bean
//ClientHttpRequestFactory是一个工厂接口,用于创建ClientHttpRequest实例,它是RestTemplate的核心组件之一,用于发送HTTP请求。
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
//simpleClientHttpRequestFactory方法返回一个SimpleClientHttpRequestFactory实例,它是ClientHttpRequestFactory的一个简单实现,
// 设置了连接和读取的超时时间,即连接超时和读取超时均为10秒。而restTemplate方法则使用factory参数来创建一个RestTemplate实例,
// 并将其返回。这样,在其他地方就可以使用RestTemplate来发送HTTP请求并处理响应了。
// SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// factory.setReadTimeout(10000);
// factory.setConnectTimeout(10000);
// return factory;
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);
// MaxTotal和MaxPerRoute是Apache HttpClient中的两个参数,用于控制HTTP连接池的大小和分配策略。
// MaxTotal指定了连接池中最大连接数的限制。这个参数控制着整个连接池的大小,包括所有的路由(即目标主机)。如果MaxTotal设置为100,那么连接池中最多可以有100个连接。
// MaxPerRoute指定了每个路由最大连接数的限制。这个参数控制着每个目标主机的连接池大小。如果MaxPerRoute设置为20,那么对于同一个目标主机,最多可以有20个连接。
// 区别在于,MaxTotal是整个连接池的大小限制,而MaxPerRoute是每个目标主机的连接池大小限制。如果需要控制整个系统中HTTP连接的总数,应该使用MaxTotal参数;如果需要控制每个目标主机的HTTP连接数,应该使用MaxPerRoute参数。
//设置连接池最大是500个连接
connectionManager.setMaxTotal(500);
//MaxPerRoute是对Maxtotal的细分,每个主机并发最大是300,route是指域名
connectionManager.setDefaultMaxPerRoute(300);
RequestConfig requestConfig=RequestConfig.custom()
//返回数据的超时时间
.setSocketTimeout(20000)
//连接上服务器的超时时间
.setConnectTimeout(10000)
//从连接池获取连接的超时时间
.setConnectionRequestTimeout(1000).build()
;
CloseableHttpClient closeableHttpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).setConnectionManager(connectionManager).build();
return closeableHttpClient;
}
}