接口效率优化总结:
1.高并发下,数据库连接池资源不足,影响读写效率
在yml配置文件 kikari配置下增加最大连接数配置 设置为60 根据机器jvm调整
maximum-pool-size: 60
#多数据源配置, 通过enable=false禁用
hikari:
# 多数据源-主数据源
master:
# 数据库连接地址
jdbc-url: jdbc:postgresql://127.0.0.1:8080/sffinterdev?useUnicode=true&characterEncoding=utf8&serverTimezone='Asia/Shanghai'&autoReconnect=true
# 数据库用户名
#username: tbaseadmin
username: devadm
# 数据库密码
# 秘钥 sff-ck-encrypt
# 云环境加密密文如下, 应用部署参数:-Dtsf_config_encrypt_password=sff-ck-encrypt
# password: ENC(HTa3ksFN2tFV83on6tItnA==)
#password: tbaseadmin@123
password: devadm@sff17
# 数据库最大连接数量
maximum-pool-size: 50
# 数据库最大等待时间
max-wait: 10000
# 数据库最小空闲数量
minimum-idle: 5
# 数据库初始化连接数量
initial-size: 5
# 验证连接
validation-query: SELECT 1
# 是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接,并尝试取出另一个
test-on-borrow: false
# 是否进行空闲测试,如果检测失败,则从连接池中去除连接
test-while-idle: true
# 在空闲连接回去线程运行期间休眠的时间值,单位毫秒
time-between-eviction-runs-millis: 18800
jdbc-interceptors: ConnectionState;SlowQueryReport(threshold=0)
# 是否启用主数据源配置项,为true才会加载上面的主数据库配置
enable: true
# 多数据源-从数据源
slave:
# 是否启用从数据源配置项,为true才会加载从数据库配置
enable: false
2.校验规则配置需要读取redis,高并发下redis连接数量不足,影响redis读取效率
修改yml redis的最大活动数量 max-active: 60
原配置为8 调整为60
redis:
# 秘钥 sff-ck-encrypt
# 云环境加密密文如下, 应用部署参数:-Dtsf_config_encrypt_password=sff-ck-encrypt
# password: ENC(m+r9qVD5rIz0XMLjweKbjg==)
password: 1q2w3e4r5t
# redis 集群
cluster:
# test:61 dev:68 dj:22
nodes: 10.186.1.68:6379
# 最大重连次数i
max-redirects: 3
# spring-boot-starter-data-redis default use lettuce pool
lettuce:
pool:
# 最大活动数
max-active: 60
# 最大空闲数量
max-idle: 8
# 最大等待数量
max-wait: -1
# 最小空闲数量
min-idle: 0
3.本地测试环境机器资源不足(就是硬件配置需要跟上)
(1)部署组原来配置实力资源限制
原配置: cpu限制 1 核 内存限制 1024MB
调整为: cpu限制 2 核 内存限制 4096MB
(2)启动参数
原设置:
-Xms128m -Xmx512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
调整为:
-Xms2048m -Xmx2048m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
Xms表示初始化java堆的大小
Xmx 表示最大可扩展的java堆大小
4.redis使用方式
校验模块原来动态配置中,每次请求都会去redis根据场景拿好几次规则,并发状态下多次与redis建立连接导致效率过高
调整为使用批量获取数据,批量处理请求。一次交互减少通信时间。
Redis的multiget方法可以一次发送多条命令,在执行完成后一次性返回结果。减少client和server的交互次数降低通信时间。
原代码:每个场景单独去读取配置信息
String rule =redisUtil.get(busScene);
修改后代码:获取所有场景后,一次性读取
List allRuleList = redisUtil.getRedisTemplate().opsForValue().multiGet(busSceneList);
5.调整cmq发送方法
由原来同步发送数据到cmq由于发送数据时数据量太大也会有一个排队的现象,所以得修改为异步发送,不用去等待。
Boolean isSendSuccess = piccCmqProducer.send(warrantyString);
调整为
piccCmqProducer.sendAsync(warrantyString);
6.调整幂等查询
在虚拟桌面本地进行压测时,发现跟数据库的连接非常慢,同一张表中如果查询,保存更新发现要么查询保存耗时,要么保存更新耗时,由于中间处理业务比较快,数据库交互间隔时间极短,加上cpu,内存等硬件设备也跟不上,所以打算优化设计,减少数据库的交互次数:
之前幂等操作是先查询如果存在,并且状态成功直接返回。每笔数据进来都会执行一次查询操作。
调整为:使用mergeinto方式,在主键冲突后,执行更新语句时加上状态不等于成功的条件,如果返回影响数为0,再去执行查询操作,确认是否真的是等于成功而导致影响数量为0导致的。
// 第一次保存
int number = interLogDAO.insertExistsUpdate(interLogPO, Constants.LOG_STATUS.SUCCESS);
if (number == Constants.NUMBER.ZERO) {
// 查询是否真的是成功状态
String logResults = findLogResults(StringUtil.trim(interLogDO.getUuid()));
if (Constants.LOG_STATUS.SUCCESS.equals(logResults)) {
throw new PiccCommonException("该uuid数据已处理,请勿重复发送。");
} else {
throw new PiccCommonException("保存或更新数据失败,请重试。");
}
}
7.反射机制调用校验方法优化
校验模块采用java反射的机制实现灵活配置,连续调用发现速度明显很慢,程序启动后,类信息与调用方法入口信息在栈中的位置是不变的。耗时主要就是在不连续的代码栈中找出位置。
基于此种情况,采用了缓存方式,就需要将类信息与调用方法存入map结构的缓存中,第一次调用时,放进map中,后面的请求就可以从map中获取。
修改为:
//缓存类信息
public static Map<String,Class> classMap = new HashMap<String,Class>();
//缓存方法
public static Map<String, Method> metMap = new HashMap<String,Method>();
(1)属性校验
public static Object cacheClassInfo(Object clazz,String method) throws NoSuchMethodException {
try {
Method m = metMap.get(method);
if(null == m) {
Class cl = classMap.get(clazz.toString());
if(null == cl) {
cl = clazz.getClass();
classMap.put(clazz.toString(),cl);
}
m = cl.getMethod(method);
metMap.put(method,m);
}
return m.invoke(clazz);
}
先从缓存中取方法对象,如果为空就缓存方法信息
再从缓存中取类对象,如果为空就缓存类对象
最后用invoke反射调用方法
8.线程池优化(上一篇文章)
原来整个清分模块就只有一个线程池,由于清分的时候会去3个后端系统都会一个个去清分,消耗时间长。因此清分模块修改为资源隔离与服务降级熔断设计思路
将调用发票微服务,缴费微服务,新收付费服务的请求,通过创建三个静态线程池实现资源隔离。
目的:
(1)某些下游服务处理请求时间过长,创建三个静态线程池,针对不同的下游服务的请求只调用各自的线程池中的线程进行处理。不会因其他下游服务消费请求耗时长导致不能正常清分数据给其他服务。实现资源隔离。
(2)实现对下游请求的限流作用,例如前端请求qps为1000次/秒。使用静态线程池,初始核心线程数为50,又因为与下游服务的请求是实时接口,故该50个请求是同步阻塞的。即:请求下游服务的qps为50次/秒。
注意事项:
静态线程池参数因根据运行环境以及业务场景并发量进行设置。参考值:若发票微服务处理一次请求的响应时间为50ms,接口引擎一个线程1s就可以处理1000/50=20个实时接口请求。若核心线程数为50,那么接口引擎1s就可以处理20*50=1000个请求,该值为理论理想值,计算过程中忽略了50个核心线程因上下文且还造成的额外消耗时间。若同时清分三个下游服务,故cpu同时处理的线程数就清分请求150个,实际运行环境的线程数量远大于此。
销项税发票效率优化点:
1.sql并表查询效率慢:
SELECT
invo.reqserialno,
invo.billstatus,
invo.billdoc,
invo.billcode,
invo.billnumber,
(
SELECT
taxpayernumber
FROM
sffinvosellerinfo seller
WHERE
seller.reqserialno = invo.reqserialno) taxpayernumber
FROM
sffinvoinfo AS invo
JOIN
sffinvoreladoc AS doc
ON
doc.reqserialno = invo.reqserialno
JOIN
sffinvoininterface AS interface
ON
interface.invoid = doc.refid::NUMERIC
WHERE
invo.billredblue=#{billRedBlue}
AND
interface.commodityno = #{policy}
AND
invo.useflag LIKE '1%'
AND
invo.billstatus IN
<foreach collection="billStatusList" item="no" index="index" open="(" separator="," close=")">
#{no}
</foreach>
GROUP BY
invo.reqserialno
通过执行计划发现此条sql在sffinvoininterface表和sffinvoreladoc表并表时进行了全表扫描导致并发时效率急剧下降,检查sffinvoininterface表的invoid字段是唯一索引,sffinvoreladoc表的refid字段和reqserialno字段时联合主键及联合唯一索引,但使用invoid和refid并表关联时并未走到索引,发现在pg中使用联合索引其中的一个字段为条件并不一定能走索引,后续为refid添加单独索引,再执行发现还是全表扫描,后尝试将条件interface.invoid = doc.refid::NUMERIC更换为interface.invoid:VARCHAR = doc.refid,再执行走到索引。
后排查所有sql,发现有一些sql相同的并表条件走到了索引,经测试后得出结论:
(1)涉及并表的字段尽量不要使用不同的类型。
(2)同样的并表条件不是在所有sql中都会使用索引,需要通过执行计划分析执行步骤,如interface.invoid = doc.refid::NUMERIC并表条件,在where条件中涉及interface表字段时需调整为interface.invoid:VARCHAR = doc.refid才能走索引,在where条件中不涉及interface表字段时,无需条件才能走索引,若调整反而无法走索引。
(3)Pgsql的联合索引不是在所有场景都生效,复杂sql需要通过执行计划分析执行步骤。
2.字符流的优化:
在压测中心测试适配老架构xml请求接口时,发现并发时效率差,通过打日志排查,发现入库转换字符集编码的filter速度很慢,发现代码中使用InputSreamReader进行字符集编码转换:
StringBuffer xml = new StringBuffer();
int b ;
try (InputStreamReader in = new InputStreamReader(input,requestCoding)){
while ((b = in.read()) != -1)
{
xml.append((char)b);
}
} catch (IOException e) {
e.printStackTrace();
}
return xml.toString();
优化后使用BufferedReader进行字符集编码转换:
StringBuilder xml = new StringBuilder();
String b;
try (BufferedReader in = new BufferedReader(new InputStreamReader(input, requestCoding))) {
while ((b = in.readLine()) != null) {
xml.append(b);
}
} catch (IOException e) {
e.printStackTrace();
}
return xml.toString();
BufferedReader类与InputSreamReader类比较:
从read()方法理解,若使用InputStreamReader的read()方法,可以发现存在每2次就会调用一次解码器解码,但若是使用 BufferedReader包装InputStreamReader后调用read()方法,可以发现只会调用一次sReader类会尽量提取比当前操作所需的更多字节,以应该更多情况下的效率提升,
因此在设计到文件字符输入流的时候,我们使用BufferedReader中包装InputStreamReader类即可
3.Xml转换Jaxb的优化:
在优化了字符流了,经过测试,在并发时效率有优化但不明显未达到理想效果,通过日志分析,发现在并发情况下xml的转换时间成倍增加,分析代码发现原代码在每次转换xml时都会new一个新的模版对象,在并发时会急剧影响效率,优化后我们将xml模版对象放入一个map,只有取不到才能new一个,若能取到就使用map中的模版,大大的提升了效率。
优化前代码:
context = JAXBContext.newInstance(obj.getClass());
优化后代码:
JAXBContext jaxbContext = jaxbContextMap.get(obj.getClass().getName());
if(StringUtil.isNull(jaxbContext)){
context = JAXBContext.newInstance(obj.getClass());
jaxbContextMap.put(obj.getClass().getName(),context);
}
总结:
在平时写代码中养成良好习惯,在new对象时需考虑此段逻辑是否会使用在并发场景,若存在考虑若何避免new重复对象。