记一次老代码优化
为什么要优化
之前经常收到服务器告警信息,CPU占用率过高,当时用jstack分析了线程状态,确认是我们在处理接口返回报文时的大写+_
转驼峰时效率太低导致的。
同时我们发现很多调用超3s的接口都是因为响应报文太长,报文转换时间太长导致的。这是个亟待解决的问题。
老代码分析
我看了下老代码,之前的处理逻辑上很简单,但是效率上真的问题有点大。服务方给我们返回的报文是xml的,我们会转成json,这里的转换都是框架里的方法,作者都是大佬,很多人都在用,这里出问题的可能性微乎其微。往下是json格式的报文中key的转换,服务方返回的key都是大写的,_
分隔单词。业务要求,我们要转成驼峰的。
而这个转驼峰的方法,是用正则表达式,从json格式的字符串中匹配"xxx":
来进行处理。正则表达式如下:
"([a-zA-z0-9_]*)":
然后一个循环,如果找到一个符合此正则的字符串,就拿出来进行处理:
- 转换成小写
- 查找
_
,将_X
替换为x
如果报文比较短小,这问题不大,如果报文很多,这个查找的过程是很麻烦的。是在这个字符串里尝试各个子序列,各种组合…… 还要在匹配的子序列里查找_
,想想就替cpu心累。
优化思路
根据老代码的分析,我们可以了解到,cpu占用率过高,应该就是在匹配正则的过程中,想想整个系统,多少qps,报文动辄几千个字符,多少子序列组合,多少次match操作。
其实最简单的方法很快就能想到,接口都是有规范的,服务提供方返回的报文,和我们需要的驼峰样式,其实就是简单的字符串替换,但是要得到对应的驼峰样式的key,免不了解析收到的报文。
第一个方案
我们第一个思路就是,写死这些key
比如,我们直接在代码里写如下的代码:
resultMap.put("aaaBbb",responContentMap.get("AAA_BBB"));
resultMap.put("cccDdd",responContentMap.get("CCC_DDD"));
resultMap.put("xxxYyy",responContentMap.get("XXX_YYY"));
这么写没什么不行,不过同事们都不同意啊,麻烦啊,再说要是万一又有什么改动,还得改啊。而且我们很多接口只是单纯的将底层的报文转驼峰返回给调用方,本来不用这些get
、put
操作的,现在都要加上,几百个接口,各种查文档,改代码,想想谁都不愿意干吧。
第二个方案
我跟领导反映,说第一个方案这种改动真的是大,虽然是不难,但是量大啊。我把心想到的方案说出来,我想建一个表,反正XXX_YYY
对应的就是xxxYyy
,不会变,我们把它记下来,然后每次报文过来,直接查一遍表,把存在的都替换了。
有人说,查数据库太慢了,几百个key,一次一次查是不是有点慢。这不算问题,我们可以用缓存,应用内缓存,guava cache、spring cache都很好用。再不济我们还可以手写一个静态map,启动的时候把数据从库里加载过来,直接把所有的key都替换一遍,不查找了。
但是这样还有个问题,这个数据表得维护啊,每次如果新增一个接口,有个没出现过的key,我们得加上啊,不然到时候这个key替换不了的啊,每次手动去搞,我真的是不行啊,不知道啥时候就忘记了。
第三个方案
我想了一下,又一次拨通了领导的电话。
这次我想,之前的算法,还得用,不过只用一次,那就是第一次。我们依然用正则匹配出所有的key,但是我们不直接去计算它对应的驼峰key,而是先去缓存查,如果有,直接替换,如果没有,还是之前的算法,整出来之后存到数据库,并加载到缓存。
当然,存数据库这个毕竟还得有一次数据库连接,至少一次数据库操作,也是耗时的嘛,所以我起一个线程,异步去操作数据库。
经过领导的同意,开始搞。
编码
- 旧方法
public static String ospInOutParmConvert(String str) {
Matcher m=p1.matcher(str);
String strTmp = "";
String strTmp1= "";
while(m.find()){
strTmp = m.group();
str = str.replace(strTmp, strTmp.toLowerCase());
if (StringUtil.isNotEmpty(strTmp) && strTmp != "null") {
strTmp1= strTmp.toLowerCase();
if (strTmp1.indexOf("_") > 0) {
String[] strTmp1s = strTmp1.split("_");
for(int i=0; i<strTmp1s.length-1; i++){
int subInt = strTmp1.indexOf("_");
str = str.replace(strTmp1.substring(subInt,subInt + 2), strTmp1.substring(subInt + 1, subInt + 2).toUpperCase());
if(strTmp1s.length>1){
strTmp1 = strTmp1.substring(subInt+1);
}
}
}
}
}
return str;
}
- 新版(无缓存)
public static String ospInOutParmConvert1(String str) {
Map<String, String> nkCamelKeys = LocalJDBCUtil.getCamelKeyByNKKeys();
for (Map.Entry<String, String> entry : nkCamelKeys.entrySet()) {
str = str.replace("\"" + entry.getKey() + "\"", "\"" + entry.getValue() + "\"");
}
return str;
}
- 新版(缓存)
public static String ospInOutParmConvert2(String str) {
Matcher m=p1.matcher(str);
String strTmp = "";
String strTmp1= "";
String nkKey = "";
String camelKey = "";
final Map<String, String> unSaveKeys = Maps.newHashMap();
while(m.find()){
strTmp = m.group();
nkKey = strTmp.replace("\"", "").replace(":", "");
camelKey = nkCamelKeyCacheService.getCamelKeyByNKKey(nkKey);
if (StringUtils.isNotBlank(camelKey)) {
str = str.replace(nkKey, camelKey);
continue;
} else {
camelKey = nkKey.toLowerCase();
str = str.replace(strTmp, strTmp.toLowerCase());
if (StringUtil.isNotEmpty(strTmp) && strTmp != "null") {
strTmp1= strTmp.toLowerCase();
if (strTmp1.indexOf("_") > 0) {
String[] strTmp1s = strTmp1.split("_");
for(int i=0; i<strTmp1s.length-1; i++){
int subInt = strTmp1.indexOf("_");
camelKey = camelKey.replace(strTmp1.substring(subInt,subInt + 2),strTmp1.substring(subInt + 1, subInt + 2).toUpperCase());
if(strTmp1s.length>1){
strTmp1 = strTmp1.substring(subInt+1);
}
}
str = str.replace(nkKey, camelKey);
}
unSaveKeys.put(nkKey, camelKey);
}
}
}
if (unSaveKeys.size() > 0) {
new Thread(new Runnable() {
@Override
public void run() {
LocalJDBCUtil.addNkCamelKeys(unSaveKeys);
}
}).start();
}
return str;
}
新的带缓存方法,也是用正则去匹配到key,然后拿key调用getCamelKeyByNKKey
方法,这个方法用了@Cacheable
注解,也就是用了spring cache来实现缓存。
下面是缓存配置类:
@Configuration
@EnableCaching
public class CacheConfig {
@Primary
@Bean
public SimpleCacheManager simpleCacheManager(List<Cache> caches) {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(caches);
return cacheManager;
}
@Bean("nkCamelKeysCache")
public ConcurrentMapCacheFactoryBean nkCamelKeysCache() {
ConcurrentMapCacheFactoryBean nkCamelKeysCache = new ConcurrentMapCacheFactoryBean();
nkCamelKeysCache.setName("nkCamelKeysCache");
return nkCamelKeysCache;
}
}
下面是缓存服务:
@Service("nkCamelKeyCacheService")
public class NKCamelKeyCacheService {
@Cacheable(value = "nkCamelKeysCache")
public String getCamelKeyByNKKey(String nkKey) {
return LocalJDBCUtil.getCamelKeyByNKKey(nkKey);
}
}
如果缓存没有命中,那么还是通过之前的老方法,不过这里我修改了一下用一个unSaveKeys
变量来记录没有保存的key映射,然后起了一个匿名线程,去调用addNkCamelKeys
方法,将unSaveKeys
中的key存进数据库。
测试
我们用一个生产环境的报文来测试,16415个字符,应该算是较大的报文了。
测试用例
@BeforeClass
public static void init() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(CacheConfig.class);
context.register(NKCamelKeyCacheService.class);
context.register(InterfacePlatformTools.class);
context.refresh();
nkCamelKeyCacheService = (NKCamelKeyCacheService) context.getBean("nkCamelKeyCacheService");
}
@Before
public void getBeginTime() {
beginTime = System.currentTimeMillis();
}
@After
public void getEndTime() {
System.out.println("耗时:" + (System.currentTimeMillis()-beginTime) + "ms");
}
@Test
public void test0() {
System.out.println("旧版驼峰转换测试结果:" + InterfacePlatformTools.ospInOutParmConvert(content));
}
@Test
public void test1() {
System.out.println("新版驼峰转换测试结果:" + InterfacePlatformTools.ospInOutParmConvert1(content));
}
@Test
public void test2() {
System.out.println("缓存版第一次(需要入库)测试结果:" + InterfacePlatformTools.ospInOutParmConvert2(content));
}
@Test
public void test3() {
System.out.println("缓存版第二次(直接在缓存中读取)测试结果:" + InterfacePlatformTools.ospInOutParmConvert2(content));
}
测试结果
旧版驼峰转换测试结果:{"errorinfo":{"message":"成功","code":0,"busiSerialNo":""
耗时:204ms
2020-03-29 19:19:46,869 INFO [main] com.cmos.crmpfcore.util.LocalJDBCUtil - ==
新版驼峰转换(无缓存)测试结果:{"errorinfo":{"message":"成功","code":0,"busiSerialNo":""
耗时:531ms
缓存版第一次(需要入库)测试结果:{"errorinfo":{"message":"成功","code":0,"busi
耗时:500ms
缓存版第二次(直接在缓存中读取)测试结果:{"errorinfo":{"message":"成功","code"
耗时:24ms
可以看出,老版本的转换方法用了204ms,每次去查数据库因为要多次连接数据库进行查询操作,需要531ms,而使用了缓存的方法,第一次我们还没有加载缓存,需要500ms,而第二次直接在缓存中读取,24ms,只用了之前方法的1/10。
如果你有更好的方案,欢迎联系我哦。