背景:
本人最近在做一个信令相关(电信行业的手机以及固话的通话记录)的项目,数据量非常大,之前也没碰到过这么大数据量,所以写程序的时候,没有意识到程序优化相关的知识,于是就出现了下面的程序。
程序一:
/** * @description: 将字符串解析为List<SignalData>对象 */ public static List<SignalData> parseData(String data) { String[] listStr = data.split("\r\n"); //data中有多条记录,每条以\r\n换行,每条记录中存在着固定的五个字段的信息,每个字段以,隔开。要求去除数据,装入对象 List<SignalData> result = new LinkedList<SignalData>(); for (String dataStr : listStr) { String[] fieldArray = dataStr.split(","); if (fieldArray.length < 5) { continue; } SignalData item = new SignalData(); for (int i = 0; i < fieldArray.length; i++) { //待优化 if (i == 0) { item.setAreaCode(fieldArray[i]); continue; } if (i == 1) { item.setCallingNum(fieldArray[i]); continue; } if (i == 2) { item.setCalledNum(fieldArray[i]); continue; } if (i == 3) { item.setCallType(fieldArray[i]); continue; } if (i == 4) { item.setSignallingTime(fieldArray[i]); continue; } } result.add(item); } return result; }
上面的代码,猛一看,for循环体中还有continue,感觉像是优化的程序。 于是就放上去运行了,之后又看了看,突然灵光一闪,感觉不对劲,既然每个字段都有数据,我为什么不直接从数组中取呢,还要循环,真是自己作死啊,好,发现一处问题,这样的程序我都看不下去了,于是赶紧优化了下,于是出现下面的代码;
优化后:
/** * @description: 将字符串解析为List<SignalData>对象 */ public static List<SignalData> parseData(String data) { String[] listStr = data.split("\r\n"); List<SignalData> result = new LinkedList<SignalData>(); for (String dataStr : listStr) { if(null==dataStr||dataStr.trim().equals("")) continue; String[] fieldArray = dataStr.split(","); if (fieldArray.length < 5) { continue; } SignalData item = new SignalData(); item.setAreaCode(fieldArray[0]); //优化之后 item.setCallingNum(fieldArray[1]); item.setCalledNum(fieldArray[2]); item.setCallType(fieldArray[3]); item.setSignallingTime(fieldArray[4]); result.add(item); } return result; }
通过这个程序我大概算了一下,平均每天8G数据,每条数据50byte, 总共是 20 * 1024 * 1024 * 8 = 167772160 条记录, 而上面的每个循环体在取数据的时候,优化之前和优化之后,在判断数组下标相等的时候每条数据总共要花掉15次, 于是 20 * 1024 * 1024 * 8 * 15 = 2516582400 ,看着数字,我的内心是崩溃的,自己的失误,每天造成多了这么多次的运算,不仅程序变慢,还要要多浪费多少电,啊啊啊啊!
程序二:
优化前:
@Override public void consume(ConsumerRecord<String, String> record) { try { //这里面是从kafka中pull数据, // pushPool 是一个静态的线程池对象,ParseAndPushTask是一个线程,record.value()就是上面的程序中的一批数据记录, pushPool.execute(new ParseAndPushTask(record.value())); // 问题部分 } catch (Exception e) { logger.error("error in PushBusinessTreate:", e); } }
这段程序,是从kafka中拉的数据,数据量就是上面的量,当写出这个程序,发布之后,直接OOM了。出现OOM赶紧停掉服务,想了一下,数据量这么大,每pull一条数据,都会生成一个线程,线程中的成员变量,也就是数据,至少在2kb的数据,也就是说,每条记录都要在堆内存中开辟2kb的数据,加上数据量大,不OOM都不正常了。。。。。
优化后:
@Override public void consume(ConsumerRecord<String, String> record) { try { PushService.receiveDataFromProducer(record.value()); //这里使用了一个静态方法 } catch (Exception e) { logger.error("error in PushBusinessTreate:", e); } }
修改之后,运行正常,这样,又节省了无数次的GC 啊
程序三:
public static List<AccountAndSubInfoVo> queryPushSubscriberData(Set<String> subscriptionIds) throws Exception { List<AccountAndSubInfoVo> result = new LinkedList<AccountAndSubInfoVo>(); for (String subscriptionId : subscriptionIds) { String subscriberId = jedisCluster.get(Constants.SUBSCRIPTION + subscriptionId); // 获取订阅者ID if (subscriberId == null || "".equals(subscriberId)) { continue; } Map<String, String> map = jedisCluster.hgetAll(Constants.SUBSCRIBER + subscriberId); if (map != null) { AccountAndSubInfoVo vo = mapToBean(map, AccountAndSubInfoVo.class); //待优化地方********** vo.setSubscriptionId(subscriptionId); vo.setSubscriberId(subscriberId); result.add(vo); } } return result; } public static <T> T mapToBean(Map<String, String> map, Class<T> obj) throws Exception { if (map == null) { return null; } Set<Entry<String, String>> sets = map.entrySet(); T t = obj.newInstance(); Method[] methods = obj.getDeclaredMethods(); for (Entry<String, String> entry : sets) { String str = entry.getKey(); String setMethod = "set" + str.substring(0, 1).toUpperCase() + str.substring(1); for (Method method : methods) { if (method.getName().equals(setMethod)) { method.invoke(t, entry.getValue()); } } } return t; }
上面的情况大概是这样的,从redis中取出相关的数据(redis中的数据类型名称已经确定),取出形式为Map类型,然后呢,我需要把Map类型转换为对象类型,之前一般情况下都是写个反射,或者是调用一些API搞定,这次也是同样,但是看了看代码,觉得需要优化,因为反射技术本身就损耗性能,况且我是知道Map中的Key的,而且字段也不多,更有效的方法就是,去掉反射,用常规的setter方法,于是,出现下面的代码
优化后:
public static List<AccountAndSubInfoVo> queryPushSubscriberData(Set<String> subscriptionIds) throws Exception { List<AccountAndSubInfoVo> result = new LinkedList<AccountAndSubInfoVo>(); for (String subscriptionId : subscriptionIds) { String subscriberId = jedisCluster.get(Constants.SUBSCRIPTION + subscriptionId); // 获取订阅者ID if (subscriberId == null || "".equals(subscriberId)) { continue; } Map<String, String> map = jedisCluster.hgetAll(Constants.SUBSCRIBER + subscriberId); if (map != null) { // AccountAndSubInfoVo vo = mapToBean(map, AccountAndSubInfoVo.class); 优化程序,反射影响性能 , AccountAndSubInfoVo vo = new AccountAndSubInfoVo(); vo.setAppId(map.get(Constants.APP_ID)); //定义的常量字符串,下同 vo.setAppSecret(map.get(Constants.APP_SECRET)); vo.setPushUrl(map.get(Constants.PUSH_URL)); vo.setSubscriptionId(subscriptionId); vo.setSubscriberId(subscriberId); result.add(vo); } } return result; }
我想,这样也肯定会省掉很多时间。
总结:
通过上面的程序,让我深深的意识到了,平时在写完代码,review一把是多么的靠谱,不但会让程序更流畅一些,最重要的的是会省很多电,节省能源!以前写Web程序,数据量不大,这些问题也没有注意到,当量发生了变化之后,许多问题都会暴漏出来,大家在看到我的程序之后,都看看自己写的程序有没有类似的问题,有则改之无则加勉,这次特意写出来,分享给大家!
下面分享一个很好的关于java程序优化的链接:
http://www.cnblogs.com/chinafine/articles/1787118.html