项目背景:由于数据库数据量的日益增加,查询效率越来越慢,为增加数据查询效率,准备将数据转移至NOSQL,NOSQL根据公司实际情况选用了redis;
我是接手了这个项目,项目刷一次全量数据到redis用时1天半,而且系统还及其不稳定,各种bug,代码结构,业务逻辑比较混乱,代码质量不高。
经过对于业务的理解,鉴于项目问题太多,决定重构。
以下是原项目中存在的明显问题:
1.框架层:dao层框架选用的是jpa
2.单线程进行redis刷新
3.业务层: 根据客户信息循环查询数据库进行数据封装,然后组装推送redis
4.业务层:全量查询数据,再判断是否需要刷新redis,在去刷新
5.单条命令推送到redis
6.定时任务用的是@Scheduled(fixedDelay={}),可以保证单线程执行,但是多个定时任务会相互等待,效率低下
为了解决以上对于效率的影响的问题:
1.框架层:经过测试使用jpa一次查询200W的数据需要的时间是12分钟,而mybatis使用的时候是19s,完全不是一个数量级。原因在于hibernates需要将查询结果转换成对象然后维护到hibernate的session缓存中,
这个非常耗时的一个过程。当大数据量查询的时候,mybatis明显是更优的选择。
1> 选用的tk_mybatis,为了加速开发效率,加入了generator进行逆向工程
2>mybatis的fetchSize默认值是100,当查询的数据达到百万级的时候,defaultFetchSize增大这个数字可以减少客户端与oracle的往返,减少响应时间; 本人设置的是10000
2. 业务逻辑:
对于百万级的数据,我们需要加快内存计算的速度,必然要引入多线程。因为每次需要处理批次的数量不相同,需要动态的去做任务分配,所以我们选用的是 forkjoin,分而治之的概念。
//forkjoin 示例代码
package com.msl.cedis.service.impl;
import com.common.base.utils.SpringContext;
import com.msl.cedis.eo.TclientItemsUw;
import com.msl.cedis.eo.TclientPolicyItemsUw;
import com.msl.cedis.service.ClientService;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Configurable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RecursiveTask;
/**
* @Author tony_t_peng
* @Date 2020-09-22 15:31
*/
@Configurable
public class SyncClientAndPolicyTask extends RecursiveTask<Integer> { //RecursiveTask RecursiveAction
private final static Logger log = LoggerFactory.getLogger(ClientServiceImpl.class);
private List<String> cliUwUids;
private int odsProcessId;
private Map<String, List<TclientItemsUw>> clientsItemsMap;
private Map<String, List<TclientPolicyItemsUw>> clientsPolicyItemsMap ;
private Map<String,Map<String,List<String>>> newPolicyInfoMap ;
public SyncClientAndPolicyTask() {
}
public SyncClientAndPolicyTask(List<String> cliUwUids, Integer odsProcessId, Map<String, List<TclientItemsUw>> clientsItemsMap, Map<String, List<TclientPolicyItemsUw>> clientsPolicyItemsMap,Map<String,Map<String,List<String>>> newPolicyInfoMap) {
this.cliUwUids = cliUwUids;
this.odsProcessId= odsProcessId;
this.clientsItemsMap=clientsItemsMap;
this.clientsPolicyItemsMap=clientsPolicyItemsMap;
this.newPolicyInfoMap=newPolicyInfoMap;
}
private ClientService clientService = SpringContext.getBean(ClientService.class);
@Override
protected Integer compute() {
int count = 0;
if (CollectionUtils.isEmpty(cliUwUids)) {
return count;
}
if (cliUwUids.size() <= 2000) {
clientService.refrushClientInfo2Redis(cliUwUids,clientsItemsMap,clientsPolicyItemsMap, newPolicyInfoMap);
return cliUwUids.size();
} else {
List<String> left = cliUwUids.subList(0, cliUwUids.size() / 2);
List<String> right = cliUwUids.subList(cliUwUids.size() / 2, cliUwUids.size());
SyncClientAndPolicyTask leftJob = new SyncClientAndPolicyTask(left,odsProcessId,clientsItemsMap,clientsPolicyItemsMap, newPolicyInfoMap);
SyncClientAndPolicyTask rightJob = new SyncClientAndPolicyTask(right,odsProcessId,clientsItemsMap,clientsPolicyItemsMap, newPolicyInfoMap);
leftJob.fork();
rightJob.fork();
return leftJob.join() + rightJob.join();
}
}
}
//调用forkjoin,其中count 就是处理的数据量
Integer count = forkJoinPool.invoke(new SyncUweCliTask(btchNo,cliUids,cliDtlMap,casCliClmMap,casCliCvgMap,casCliPolMap,glhCliClmMap,glhCliCvgMap,glhCliPolMap));
3和4:1.用批次号控制一次循环处理的数据量,我这边一次循环的处理量大概在200W数据以内。
2.对于一次循环需要刷新的数据用sql条件先进行过滤。减少加载到内存的数据量。
3.一次全量查询出一个批次内可能用到的各种数据并转换成map,然后直接交给内存运算
目的:1.减少数据加载到内存的数据量,做到查询的结果就是需要刷新的数据 2. 批量查出一个批次所有需要的数据,业务逻辑全内存处理无sql等待。
5. 数据刷新到redis,使用管道批量刷新,减少连接获取,资源关闭的开销。 同时因为redis服务是单线程的,需要控制管道的命令量不要过分多,因为管道命令过多执行可能会导致redis线程阻塞,导致其他线程操作redis超时。所以需要控制管道的命令量,并且适当扩大redis的超时时间. 可以改为60s或者100秒应该足够了
6.定时任务改为quaze,同时对于同一个任务做到单线程启动。加上注解@DisallowConcurrentExecution
实现上面所有的功能点,项目有之前刷新redis的1天半已经可以跑到24分钟以内,一个批次200W以内的数据,都是秒级刷新redis,基本实现了项目的效率期往