写在前面
性能优化是一个包含很广有深度的知识,相对于自己浅显有限的经验实在有点难度。这里只是记录自己工作中一点常见的问题。
正文
性能说明:代码执行的通常主要瓶颈在第三方上(即其它的地方执行速度),例如磁盘IO 和 网络 IO,由于网络IO,取决于网络条件,更是有很大不确定性如延迟,所以通常如果优化代码速度,首要考虑的就是这一方面。
一、在 循环里 里调用数据库(包括缓存数据库)查询或更新
这个是最常见的,因为这样做符合思维模式。但是这很容易拖慢速度。原因在于,数据库操作本身有IO和CPU,即使这个时间很快,基本也不可能快过正常代码的执行,而且数据传输又通常通过网络,因此其实这种数据库操作是远远小于代码执行的速度的。
1,循环中查缓存和数据库的例子及优化
public CommonResult judgeRoleCsLimit(List<CsChangeVo> csChangeVos) {
CommonResult commonResult = new CommonResult();
for(CsChangeVo csChangeVo:csChangeVos){
if(csChangeVo.hasCsEntityId()){
Integer assignCsCount = csChangeVo.getCsEntityIds().size();
//这里进行了缓存的查询
EmployeePo employeePo = EmployeeCache.get(csChangeVo.getEmployeeId());
//这里进行了数据库的查询
RolePo rolePo = roleService.getByEmpMaxCsLimit(csChangeVo.getEmployeeId());
if(rolePo != null && employeePo != null && rolePo.getCsLimit() != null){
Integer csLimit = rolePo.getCsLimit();
Integer csCount = employeePo.getCsCount();
if(csLimit != 0 && assignCsCount + csCount > csLimit){
commonResult.setSuccess(false);
return commonResult;
}
}
}
}
commonResult.setSuccess(true);
return commonResult;
}
修改后:
public CommonResult judgeRoleCsLimit(List<CsChangeVo> csChangeVos) {
CommonResult commonResult = new CommonResult();
List<String> employeeIdList = new ArrayList<>();
for(CsChangeVo csChangeVo:csChangeVos){
if(csChangeVo.hasCsEntityId()){
employeeIdList.add(csChangeVo.getEmployeeId());
}
}
List<EmployeePo> employeePos = employeeRepository.findByIds(employeeIdList);
Map<String,EmployeePo> employeePoMap = new HashMap<>();
if(CollectionUtils.isNotEmpty(employeePos)) {
for (EmployeePo employeePo : employeePos) {
employeePoMap.put(employeePo.getId(), employeePo);
}
}
List<RolePo> rolePos = roleService.findByEmpMaxCsLimit(employeeIdList);
Map<String, RolePo> userIdRolePoMap = new HashMap<>();
if(CollectionUtils.isNotEmpty(rolePos)) {
for (RolePo rolePo : rolePos) {
userIdRolePoMap.put(rolePo.getUserId(), rolePo);
}
}
for(CsChangeVo csChangeVo:csChangeVos){
if(csChangeVo.hasCsEntityId()){
Integer assignCsCount = csChangeVo.getCsEntityIds().size();
EmployeePo employeePo = EmployeeCache.get(csChangeVo.getEmployeeId());
RolePo rolePo = userIdRolePoMap.get(csChangeVo.getEmployeeId());
if(rolePo != null && employeePo != null && rolePo.getCsLimit() != null){
Integer csLimit = rolePo.getCsLimit();
Integer csCount = employeePo.getCsCount();
// 如果员工客户数量 超过角色客户上限,不允许分配
if(csLimit != 0 && assignCsCount + csCount > csLimit){
commonResult.setSuccess(false);
return commonResult;
}
}
}
}
commonResult.setSuccess(true);
return commonResult;
}
类似比较常见的列表查关联数据的时候,即获取的主对象列表需要带出子对象(一对多)。例如下面的代码:
for(ProdEntityPo prodEntityPo: prodEntityPoList){
ProdImgPo prodImgPo = prodImgPoMap.get(prodEntityPo.getId());
if(prodImgPo!=null){
prodEntityPo.setPath(prodImgPo.getPath());
}
CatePo catePo = cateCache.get(prodEntityPo.getCateId());
if(catePo != null){
prodEntityPo.setCate(catePo.getName());
}
List<ProdSkuPo> prodSkuPoList = null;
if(prodEntityPo.getType().contains("sku")){
prodSkuPoList = prodSkuQueryDao.findByProdEntityId(prodEntityPo.getId());
}else{
ProdSkuPo prodSkuPo = prodSkuQueryDao.getDefaultSku(prodEntityPo.getId());
if(prodSkuPo != null){
prodSkuPoList = new ArrayList<>();
prodSkuPoList.add(prodSkuPo);
}
}
prodEntityPo.setProdSkuPoList(prodSkuPoList);
}
简单修改:
List<String> prodEntityIdList = new ArrayList<>(prodEntityPoList.size());
for (ProdEntityPo prodEntityPo : prodEntityPoList) {
prodEntityIdList.add(prodEntityPo.getId());
}
List<ProdSkuPo> prodSkuPos = prodSkuQueryDao.findByProdEntityIdList(prodEntityIdList);
Map<String, List<ProdSkuPo>> prodSkuListMap = new HashMap<>();
for (ProdSkuPo prodSkuPo : prodSkuPos) {
List<ProdSkuPo> tmpList = prodSkuListMap.get(prodSkuPo.getSaleProdId());
if(tmpList==null) {
tmpList = new ArrayList<>();
}
tmpList.add(prodSkuPo);
prodSkuListMap.put(prodSkuPo.getSaleProdId(), tmpList);
}
for(ProdEntityPo prodEntityPo: prodEntityPoList){
ProdImgPo prodImgPo = prodImgPoMap.get(prodEntityPo.getId());
if(prodImgPo!=null){
prodEntityPo.setPath(prodImgPo.getPath());
}
CatePo catePo = cateCache.get(prodEntityPo.getCateId());
if(catePo != null){
prodEntityPo.setCate(catePo.getName());
}
List<ProdSkuPo> prodSkuPoList = null;
if(prodEntityPo.getType().contains("sku")){
prodSkuPoList = prodSkuListMap.get(prodEntityPo.getId());
}else{
List<ProdSkuPo> tmpList = prodSkuListMap.get(prodEntityPo.getId());
ProdSkuPo prodSkuPo = null;
for (ProdSkuPo tmpPo : tmpList) {
if(Bool.y.name().equals(tmpPo.getIsDefault())) {
prodSkuPo = tmpPo;
break;
}
}
if(prodSkuPo != null){
prodSkuPoList = new ArrayList<>();
prodSkuPoList.add(prodSkuPo);
}
}
prodEntityPo.setProdSkuPoList(prodSkuPoList);
}
再简单说一下:
为什么这样子可以优化,假设一个for循环时间复杂度粗略看为T(n),那么优化后“看起来”好像是4个T(n),而且代码看起来好像复杂了,怎么后者效率反而高了呢。
其实就是上面提到的,减少了网络和IO的次数。将网络和IO的次数由不确定的n次转换为确定的次数。随着n的增大,差距越明显。通过将多次网络转化几次网络和SQL之后,再放到内存Map中进行查询获取<查表法>,减少了对第三方网络和IO的依赖,而在内存中代码的执行速度是远远快过IO的,因此可以将代码执行速度大大提升。循环里查数据库只有在确定较少次数的情况下使用才比较合理,例如,我确定这个循环最高次数只有5次,那么可以使用。
2,循环中更新数据库的例子及优化
更新(插入、删除、修改)本身带有批量性质,因此修改起来更加容易。如插入,将循环中对象加入list,在结束循环后进行insert即可。要注意的是 insert 的数据量是有限制的,一般在1000条记录以下。这个情况下,可以简单使用多线程来进行插入,如下:
private void multiThreadBatchAdd(List<CsEntityPo> createCsEntityPos, final ResultVO resultVO) {
int threadCount = 3;
ExecutorService executorService = ThreadPoolUtils.createFixedPool(threadCount);
int totals = createCsEntityPos.size();
int pageSize = 50;
int pages = totals / pageSize + 1;
for (int i = 0; i < pages; i++) {
final List<CsEntityPo> csEntityPos = createCsEntityPos.subList(i * pageSize, totals > (i + 1) * pageSize ? (i + 1) * pageSize : totals);
executorService.submit(new Runnable() {
@Override
public void run() {
try {
if (!resultVO.hasException) {
batchAdd(csEntityPos);
}
} catch (Exception e) {
resultVO.hasException = true;
resultVO.msg = e.getMessage();
}
}
});
}
ThreadPoolUtils.awaitFinish(executorService, new ThreadExe() {
@Override
public void onWait() {
}
@Override
public void onFinish() {
}
});
if (resultVO.hasException) {
throw new RuntimeException(resultVO.msg);
}
}
然而,在小批量的插入里,上面的方式就足够里,然而更大批量的插入里,由于每条插入都要更新索引等,这种方式数据库操作如mysql其实不够快,数据库有更快的方式进行插入,mysql里,则是load导入。更加快速的两个原因,一是因为使用二进制流传送插入数据,二则是在操作完成之后才进行索引、binlog等的更新。当然还有更多细节。
关于load,更详细可以查看文档:https://dev.mysql.com/doc/refman/5.7/en/load-data.html
下面是我的一点代码:
几个注意点:1)斜杠转义,斜杠本身也需要转义,因此使用双斜杠进行转义 2)空值 \N 3)为使字段按照自己对应的来,最好指明字段顺序(类似insert)
public <T> int executeBatch(TableEntity tableEntity, List<T> datas, boolean isInsert) throws GeneralException, SQLException {
Connection conn = null;
PreparedStatement st = null;
File tmpFile = null;
try {
conn = getConnection();
String filePath = buildFilePath(tableEntity, datas);
st = conn.prepareStatement(buildLoadSql(tableEntity, filePath,true,isInsert));
int result = 0;
result=st.executeUpdate();
tmpFile = new File(filePath);
if(tmpFile.exists()) {
tmpFile.delete();
}
return result;
} catch (Exception e) {
throw new GeneralException(this.getClass(), e);
} finally {
if(tmpFile!=null && tmpFile.exists()){
tmpFile.delete();
}
closeConn(null, st, conn);
}
}
public <T> String buildFilePath(TableEntity tableEntity, List<T> datas) {
String text = buildText(tableEntity, datas);
try {
String name=System.nanoTime() + ".sql";
File fileName = new File(name);
if (!fileName.exists()) {
fileName.createNewFile();
}
FileWriterWithEncoding fileWriter = new FileWriterWithEncoding(fileName, "UTF-8");
fileWriter.write(text);
fileWriter.flush();
fileWriter.close();
return name;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public <T> String buildText(TableEntity tableEntity, List<T> datas) {
StringBuilder builder = new StringBuilder();
for (T object : datas) {
for (TableColum colum : tableEntity.getTableColums()) {
@SuppressWarnings("unchecked")
String value = colum.getColumnMapper().getColumnVal(colum, object);
if (StringUtils.isBlank(value)) {
value = "\\N";
}
builder.append("'").append(StringEscapeUtils.escapeSql(value)).append("'").append(",");
}
builder.append("\r\n");
}
String text = builder.toString();
return text;
}
/**
*注意的几个点:
*local:文件在本地,不写则说明文件在远程数据库服务器上。
*replace和ignore: 在遇到重复(唯一限制字段重复如主键或加了唯一索引的字段)替换还是跳过
* fields terminated by ',' : 字段以 ',' 逗号分割
* lines terminated by '\\r\\n' :记录以 \\r\\n 分割
* ...
*/
private String buildLoadSql(TableEntity tableEntity, String filePath,boolean isLocalPath,boolean isReplace) {
String localStr =isLocalPath?"local":"";
String operateStr = isReplace?"replace":"ignore";
String sql = " load data "+localStr+" infile '" + filePath + "' "+operateStr+" into table " + getTableName(tableEntity)
+ " character set utf8 fields terminated by ',' enclosed by '\\'' lines terminated by '\\r\\n' " + getColumnStr(tableEntity);
return sql;
}
private String getTableName(TableEntity tableEntity) {
String tableName = "";
if (StringUtils.isNotBlank(tableEntity.getDbName())) {
tableName = tableEntity.getDbName() + ".";
}
return tableName = tableName + tableEntity.getTableName();
}
对于修改更新,如果每个对象要更新的值都不一样,并且没有规则(如原字段+1),无法直接使用确定的几条update进行更新的,也可以load的方式进行replace更新。