记一次老代码优化

记一次老代码优化

为什么要优化

之前经常收到服务器告警信息,CPU占用率过高,当时用jstack分析了线程状态,确认是我们在处理接口返回报文时的大写+_转驼峰时效率太低导致的。

同时我们发现很多调用超3s的接口都是因为响应报文太长,报文转换时间太长导致的。这是个亟待解决的问题。

老代码分析

我看了下老代码,之前的处理逻辑上很简单,但是效率上真的问题有点大。服务方给我们返回的报文是xml的,我们会转成json,这里的转换都是框架里的方法,作者都是大佬,很多人都在用,这里出问题的可能性微乎其微。往下是json格式的报文中key的转换,服务方返回的key都是大写的,_分隔单词。业务要求,我们要转成驼峰的。
而这个转驼峰的方法,是用正则表达式,从json格式的字符串中匹配"xxx":来进行处理。正则表达式如下:

"([a-zA-z0-9_]*)":

然后一个循环,如果找到一个符合此正则的字符串,就拿出来进行处理:

  1. 转换成小写
  2. 查找_,将_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"));

这么写没什么不行,不过同事们都不同意啊,麻烦啊,再说要是万一又有什么改动,还得改啊。而且我们很多接口只是单纯的将底层的报文转驼峰返回给调用方,本来不用这些getput操作的,现在都要加上,几百个接口,各种查文档,改代码,想想谁都不愿意干吧。

第二个方案

我跟领导反映,说第一个方案这种改动真的是大,虽然是不难,但是量大啊。我把心想到的方案说出来,我想建一个表,反正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。

如果你有更好的方案,欢迎联系我哦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值