記一次老代碼優化


記一次老代碼優化

為什麼要優化

之前經常收到服務器告警信息,CPU佔用率過高,當時用jstack分析了線程狀態,確認是我們在處理接口返回報文時的大寫+_轉駝峰效率太低導致的。

同時我們發現很多調用超3s的接口都是因為響應報文太長,報文轉換時間太長導致的。這是個亟待解決的問題。


老代碼分析

我看了下老代碼,之前的處理邏輯上很簡單,但是效率上真的問題有點大。服務方給我們返回的報文是xml的,我們會轉成json,這裡的轉換都是框架裡的方法,作者都是大佬,很多人都在用,這裡出問題的可能性微乎其微。往下是json格式的報文中key的轉換,服務方返回的key都是大寫的,_分隔單詞。業務要求,我們要轉成駝峰的。
而這個轉駝峰的方法,是用正則表達式,從json格式的字符串中匹配"xxx":來進行處理。正則表達式如下:


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

/<code>

然後一個循環,如果找到一個符合此正則的字符串,就拿出來進行處理:

  1. 轉換成小寫
  2. 查找_,將_X替換為x

如果報文比較短小,這問題不大,如果報文很多,這個查找的過程是很麻煩的。是在這個字符串裡嘗試各個子序列,各種組合…… 還要在匹配的子序列裡查找_,想想就替cpu心累。


優化思路

根據老代碼的分析,我們可以瞭解到,cpu佔用率過高,應該就是在匹配正則的過程中,想想整個系統,多少qps,報文動輒幾千個字符,多少子序列組合,多少次match操作。

其實最簡單的方法很快就能想到,接口都是有規範的,服務提供方返回的報文,和我們需要的駝峰樣式,其實就是簡單的字符串替換,但是要得到對應的駝峰樣式的key,免不了解析收到的報文。


第一個方案

我們第一個思路就是,寫死這些key

比如,我們直接在代碼裡寫如下的代碼:


<code>resultMap.put("aaaBbb",responContentMap.get("AAA_BBB"));
resultMap.put("cccDdd",responContentMap.get("CCC_DDD"));
resultMap.put("xxxYyy",responContentMap.get("XXX_YYY"));
/<code>

這麼寫沒什麼不行,不過同事們都不同意啊,麻煩啊,再說要是萬一又有什麼改動,還得改啊。而且我們很多接口只是單純的將底層的報文轉駝峰返回給調用方,本來不用這些get、put操作的,現在都要加上,幾百個接口,各種查文檔,改代碼,想想誰都不願意幹吧。


第二個方案

我跟領導反映,說第一個方案這種改動真的是大,雖然是不難,但是量大啊。我把新想到的方案說出來,我想建一個表,反正XXX_YYY對應的就是xxxYyy,不會變,我們把它記下來,然後每次報文過來,直接查一遍表,把存在的都替換了。

有人說,查數據庫太慢了,幾百個key,一次一次查是不是有點慢。這不算問題,我們可以用緩存,應用內緩存,guava cache、spring cache都很好用。再不濟我們還可以手寫一個靜態map,啟動的時候把數據從庫里加載過來,直接把所有的key都替換一遍,不查找了。

但是這樣還有個問題,這個數據表得維護啊,每次如果新增一個接口,有個沒出現過的key,我們得加上啊,不然到時候這個key替換不了的啊,每次手動去搞,我真的是不行啊,不知道啥時候就忘記了。


第三個方案

我想了一下,又一次撥通了領導的電話。
這次我想,之前的算法,還得用,不過只用一次,那就是第一次。我們依然用正則匹配出所有的key,但是我們不直接去計算它對應的駝峰key,而是先去緩存查,如果有,直接替換,如果沒有,還是之前的算法,整出來之後存到數據庫,並加載到緩存。

當然,存數據庫這個畢竟還得有一次數據庫連接,至少一次數據庫操作,也是耗時的嘛,所以我起一個線程,異步去操作數據庫。

經過領導的同意,開始搞。


編碼

  • 舊方法


<code>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> 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;
}
/<strtmp1s.length-1>/<code>
  • 新版(無緩存)


<code>    public static String ospInOutParmConvert1(String str) {
Map<string> nkCamelKeys = LocalJDBCUtil.getCamelKeyByNKKeys();
for (Map.Entry<string> entry : nkCamelKeys.entrySet()) {
str = str.replace("\"" + entry.getKey() + "\"", "\"" + entry.getValue() + "\"");
}
return str;
}
/<string>/<string>/<code>
  • 新版(緩存)


<code>public static String ospInOutParmConvert2(String str) {
Matcher m=p1.matcher(str);
String strTmp = "";
String strTmp1= "";
String nkKey = "";
String camelKey = "";
final Map<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> 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;
}
/<strtmp1s.length-1>/<string>/<code>

新的帶緩存方法,也是用正則去匹配到key,然後拿key調用getCamelKeyByNKKey方法,這個方法用了@Cacheable註解,也就是用了spring cache來實現緩存。

下面是緩存配置類:


<code>@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;
}
}
/<cache>/<code>

下面是緩存服務:


<code>@Service("nkCamelKeyCacheService")
public class NKCamelKeyCacheService {
@Cacheable(value = "nkCamelKeysCache")
public String getCamelKeyByNKKey(String nkKey) {
return LocalJDBCUtil.getCamelKeyByNKKey(nkKey);
}
}
/<code>

如果緩存沒有命中,那麼還是通過之前的老方法,不過這裡我修改了一下用一個unSaveKeys變量來記錄沒有保存的key映射,然後起了一個匿名線程,去調用addNkCamelKeys方法,將unSaveKeys中的key存進數據庫。


測試

我們用一個生產環境的報文來測試,16415個字符,應該算是較大的報文了。


測試用例


<code>@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));
}
/<code>


測試結果


<code>舊版駝峰轉換測試結果:{"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
/<code>

可以看出,老版本的轉換方法用了204ms,每次去查數據庫因為要多次連接數據庫進行查詢操作,需要531ms,而使用了緩存的方法,第一次我們還沒有加載緩存,需要500ms,而第二次直接在緩存中讀取,24ms,只用了之前方法的1/10。

如果你有更好的方案,歡迎聯繫我哦。


分享到:


相關文章: