📝 一、并发安全问题
串行化的执行代码是理想情况,在会产生并发问题导致数据不一致的地方合理使用锁是必要的。
- 案例:从库里随机获取一条 is_used = 0 的数据,将此数据塞到主表中,更新主表,再更新is_used = 1
压测中遇到的问题是:
- 同一个用户同一时刻并发请求更新主表失败
- 从库里随机获取一条数据重复
解决问题一:
- 对同一个用户加分布式锁。之前该代码是对请求参数中唯一身份信息进行了分布式加锁的,但是针对同一个账号的维度没有加锁。由此对底层代码进行修改,支持分布式锁切面中的”,”表达式。也可以在代码中直接获取锁。Redission支持了这种多个锁的获取,叫做RedissonMultiLock。使用起来也十分方便。Redission分布式锁
- 为什么会报错?公司的底层框架默认继承了乐观锁,当一个线程更新完成以后,另一个线程进行更新data_version不一致则报错。加入用户id维度的分布式锁即可解决此问题。
在解决这个问题的过程中,我还遇到了一个问题,就是当A、B线程同时进入方法时,A线程获取到了锁并执行完方法后,B线程进入代码并未获取到A线程更新完成的数据,导致又一次更新主表。
当时很快想到了事务的问题,mysql默认事务隔离级别是可重复读,肯定就是A线程的事务未提交。一看代码:
@Transactional @DistributeRedisLock(lockKey = "#id,#idNo") public void doSomething(String id,String idNo){ //must need do something... }毫无疑问,@Tracnsactional的优先级大于自定义的获取锁的redis切面,当redis分布式锁切面完成以后,事务才提交。那解决方法就很多了,从声明式事务改为手动式事务或者不用分布式锁切面,又或者调整@Order都是可以的。
解决问题二:
加上for_update,为什么加上for_update就可以了?
select * from t_code where is_used = 0 for update;🤐 二、超时问题
IO密集型应用,并发一高大家都会知道先加点缓存,但是请求http接口也是需要加缓存的。
1、ssl握手问题
代码中,使用了restTemplate请求外部接口,其中基本大多都为https接口。偶发的出现这个错误
- I/O error on POST request for "https://xxxx.com/xxx": peer not authenticated; nested exception is javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
在网上搜才知道是ssl握手失败,restTemplate中需要指定ssl握手策略即可解决。
解决方法:
TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> {
return true;
};
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
factory.setConnectTimeout(httpClientProperties.getConnectTimeout());
factory.setReadTimeout(httpClientProperties.getReadTimeout());
factory.setConnectionRequestTimeout(httpClientProperties.getConnectTimeout());
return factory;2、ssl连接耗时过长
这个问题解决了很久,请求”https://www.xxx.com/getData”接口耗时在200ms,200ms也是比较正常的数字,但是这个接口在并发10到20中有耗时的增加,导致tps一直上不去。
排查问题:
1、首先考虑的是对方服务耗时是不是很高,看了对方服务内耗时和nginx耗时,都是个位数ms耗时
2、考虑是不是restTemplate本身的问题,使用curl -v -w 打印耗时,结果为:

其中:
time_namelookup:DNS 域名解析的时候,就是把https://www.baidu.com转换成 ip 地址的过程time_connect:TCP 连接建立的时间,就是三次握手的时间time_appconnect:SSL/SSH 等上层协议建立连接的时间,比如 connect/handshake 的时间time_redirect:从开始到最后一个请求事务的时间time_pretransfer:从请求开始到响应开始传输的时间time_starttransfer:从请求开始到第一个字节将要传输的时间time_total:这次请求花费的全部时间
发现,ssl耗时最高,解决方法:在redisTemplate中加入ssl连接池,完整代码:
@Bean
@ConditionalOnMissingBean(name = {"httpsClientHttpRequestFactory"})
ClientHttpRequestFactory httpsClientHttpRequestFactory(HttpClientProperties httpClientProperties) {
if (!httpClientProperties.isUsePool()) {
HttpsClientRequestFactory factory = new HttpsClientRequestFactory();
factory.setConnectTimeout(httpClientProperties.getConnectTimeout());
factory.setReadTimeout(httpClientProperties.getReadTimeout());
return factory;
} else {
CloseableHttpClient httpClient;
try {
TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> {
return true;
};
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", connectionSocketFactory)
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(20);
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setSSLSocketFactory(connectionSocketFactory);
httpClientBuilder.setConnectionManager(connectionManager);
httpClientBuilder.setConnectionManagerShared(true);
httpClient = httpClientBuilder.build();
} catch (Exception var7) {
throw new RuntimeException("create httpsClientHttpRequestFactory failed", var7);
}
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
factory.setConnectTimeout(httpClientProperties.getConnectTimeout());
factory.setReadTimeout(httpClientProperties.getReadTimeout());
factory.setConnectionRequestTimeout(httpClientProperties.getConnectTimeout());
return factory;
}
}关于更详细的curl使用方法,参考:使用 curl 命令分析请求的耗时情况
📎 其他
压测指标
10个人同时发起请求,并发为10,请求耗时100ms,那TPS为 1s / 100ms * 10并发 = 100TPS