需求
大约200W条数据,批量从mysql中读取,然后根据主键再从hbase读数据进行关联,最后再update到数据库中
同步解决方案
同步解决方案,也是最接近人脑思考顺序的方案是,分页mysql读取id集合,每页1k条数据,然后拿着idList批量从nosql的hbase中进行数据的获取,进行数据的封装,然后逐条更新到数据库中。实验结果表明,如果要完成这项工作,估计要10小时以上。
先做个简单的优化,尽可能的降低io开销,将逐条更新回数据库修改成,延迟批量提交数据库。这样1k次io开销缩减成1次。利用ibatis的批量提交特性,具体代码如下
@Override
public void batchUpdate(final List<T> objectList) {
this.getSqlMapClientTemplate().execute(new SqlMapClientCallback() {
public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
executor.startBatch();
for (T tmp : objectList) {
executor.update(sqlmapNamespace + ".update" + , tmp);
}
return executor.executeBatch();
}
});
}
这样再次实验后,发现跟新1k条数据从原来的10s以上降低到300ms左右,还是有着非常大的提升的。整体的1k的分页任务完成,从原来的40s左右降低到1.5s左右。那么完成200w左右的数据,仍然需要接近1个小时,不能满足业务期望
异步解决方案
IO的开销,已经基本上没办法在低成本的角度去优化了。那么可以从cpu的角度进行提高,运行top命令后发现,cpu的java占比基本在%0.3以内。因此可以尝试采用多线程的异步方案进行并发处理。起线程的代码如下,起大约10个线程左右,起的太多,会造成数据库连接数超出,导致数据库连接异常
query.setCurrentPage(1);
query.setPageSize(100000);// 开10个线程左右,能覆盖200W的数据
Integer totalInteger = rUserAlipayDAO.countByQuery(query);
query.setTotalItem(totalInteger);
do {
try {
GetDataThread thread = new GetDataThread(query.getStartRow(), query.getEndRow());
Thread t = new Thread(thread);
t.start();
} catch (Throwable t) {
logger.error("update error", t);
}
} while (query.nextPage());
GetDataThread 代码如下,接受10W条左右的数据,进行任务的操作,构造函数如下,主要用于区分每个线程处理的起始位置
public GetDataThread (Integer startRow, Integer endRow){
this.startRow=startRow;
this.endRow=endRow;
}
下面是处理的run方法
@Override
public void run() {
...
query.setStartRow(startRow);
query.setPageSize(1000);//每页1K条数据
logger.warn("do startRow"+startRow +" thread start...");
Long startLong = System.currentTimeMillis();
do{
//TODO 进行相关的任务处理
startRow= startRow+1000;
query.setStartRow(startRow);
}while(startRow<endRow);
logger.warn("Thread startRow"+startRow +" cost "+(System.currentTimeMillis()-startLong));
}
调整后,每个线程处理10W条数据,大概2分钟左右完成,但是由于是10个线程同时开工,总体任务的执行时间基本控制在3min以内,完全满足业务期望
总结
这个场景非常简单,写这篇文章的目的主要是找到一个案例,让初学者了解如何去分析一段代码存在的性能问题,并且如何针对这些问题进行代码改进。