关于JAVA代码性能优化一点记录

14 篇文章 0 订阅
9 篇文章 0 订阅

写在前面

性能优化是一个包含很广有深度的知识,相对于自己浅显有限的经验实在有点难度。这里只是记录自己工作中一点常见的问题。

正文

性能说明:代码执行的通常主要瓶颈在第三方上(即其它的地方执行速度),例如磁盘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更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值