前天晚上加班完成部门Q4KPI考核计划后,看到业务开发组的几个小伙伴在处理生产问题。我上前了解情况。
销管系统,客户交易明细页面,查询客户交易数据的逻辑是:调用远程数据中心接口,拿到原始交易数据集合,然后在内存里通过相关id来给客户名称、服务商名称、销售人员名称、所属部门、上级销售主管赋值。
产品经理反馈,销售主管登陆系统查询数据时,非常慢,慢到4~5分钟。
查看日志,发现后台程序处理耗时动辄高达300s。
300s是个庞大的数字!当务之急,是看能不能降低到10s以内。通过分析,其中,获取远程交易数据耗时≈6s,本地内存数据匹配竟然耗时200多秒,incredible!unbelievable!
那接下来要对各个匹配数据的程序段来分析。通过细化耗时,发现在for循环匹配销售数据为销售人员名称、所属部门、上级销售主管赋值时,异常地慢。
贴出来这段代码:
/** * 查询销售与部门的关联关系
* @param saleId
* @return
*/
public CommonRequestDTO selectSaleDepartRelation(Integer saleId){
List<CommonRequestDTO> relationList = CacheUtil.getCache(SaleCommonConstant.SALE_DEPART_RELATION, SaleCommonConstant.EXPIRY_SECONDS, ()
-> emaxSalerMapper.selectSaleDepartRelation()
);
relationList = relationList.stream().filter(o -> saleId.equals(o.getSaleId())).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(relationList)){
CommonRequestDTO commonRequestDTO = relationList.get( 0 );
commonRequestDTO.setSaleName(commonRequestDTO.getSaleName());
commonRequestDTO.setDepartName(commonRequestDTO.getDepartName());
commonRequestDTO.setDepartHeaderName(commonRequestDTO.getDepartHeaderName());
return commonRequestDTO;
}
return null ;
} |
其中,CacheUtil封装了Redis的get/set操作。
emaxSalerMapper#selectSaleDepartRelation是查数据库获取基础关系数据,共223条数据,耗时6~7ms。
CommonRequestDTO是一个pojo模型类。
那么,这段代码也看不出哪里慢呀!
仔细一分析,发现端倪。Cc同学怀疑问题出在读redis上。果不其然,for循环里频繁调用redis获取集合数据,尤其是当查询数据记录多循环次数多时,必然拉跨。
当务之急,最好的解决办法,是用本地缓存来搞,HutoolCache登场。
static TimedCache<String ,List<CommonRequestDTO>> cache= cn.hutool.cache.CacheUtil.newTimedCache(SaleCommonConstant.EXPIRY_SECONDS);
/** * 查询销售与部门的关联关系
* @param saleId
* @return
*/
public CommonRequestDTO selectSaleDepartRelation(Integer saleId){
if (cache.get(SaleCommonConstant.SALE_DEPART_RELATION)== null ){
cache.put(SaleCommonConstant.SALE_DEPART_RELATION, emaxSalerMapper.selectSaleDepartRelation());
}
List<CommonRequestDTO> relationList = cache.get(SaleCommonConstant.SALE_DEPART_RELATION);
relationList = relationList.stream().filter(o -> saleId.equals(o.getSaleId())).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(relationList)){
CommonRequestDTO commonRequestDTO = relationList.get( 0 );
commonRequestDTO.setSaleName(commonRequestDTO.getSaleName());
commonRequestDTO.setDepartName(commonRequestDTO.getDepartName());
commonRequestDTO.setDepartHeaderName(commonRequestDTO.getDepartHeaderName());
return commonRequestDTO;
}
return null ;
} |
改造完成,再测试,发现这段代码耗时已经到ms级了。整体方法耗时也控制在了10s以内。
那么,回过头来分析,我们看程序里redis-RedisTemplate配置,valueSerializer使用Jackson2JsonRedisSerializer,Jackson2JsonRedisSerializer序列化使用ObjectMapper。
/** * RedisTemplate配置
* @param lettuceConnectionFactory
* @return
*/
@Bean public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
// 设置序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object. class );
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, Visibility.ANY);
om.enableDefaultTyping(DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
RedisSerializer<?> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer); // key序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value序列化
redisTemplate.afterPropertiesSet();
return redisTemplate;
} |
ObjectMapper在序列化时,会将所有的字段序列化,无论这些字段是否有值(是否为null)。再看CommonRequestDTO,有多达22个属性,可见在本案List<CommonRequestDTO>中有223个元素时,数据体积无形中增大很多。通过下面对ObjectMapper的测试代码来比较一下,很明显可以看到单个对象序列化后在数据量方面的差异:
@Test public void testObjectMapper2() throws JsonProcessingException {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY)
.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
System.out.println( new String(om.writeValueAsBytes( new CommonRequestDTO())));
om = new ObjectMapper();
om.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY)
.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
System.out.println( "不序列化空值字段 ↓ ↓ ↓" );
System.out.println( new String(om.writeValueAsBytes( new CommonRequestDTO())));
} |
序列化所有字段(无论这些字段是否有值) ↓ ↓ ↓ [ "com.emax.memberaccount.restapi.vo.CommonRequestDTO" ,{ "enterpriseId" : null , "enterpriseBizId" : null , "enterpriseName" : null , "saleId" : null , "product" : null , "entStatus" : null , "departId" : null , "agentId" : null , "levyId" : null , "departHeaderId" : null , "saleIds" : null , "enterpriseIds" : null , "productList" : null , "createTimeBegin" : null , "createTimeEnd" : null , "saleName" : null , "departName" : null , "departHeaderName" : null , "ifDepartHeader" : null , "loginSalerId" : null , "selectEnterpriseId" : null , "orderEndTime" : null , "enterpriseProductDTOS" : null }]
不序列化空值字段 ↓ ↓ ↓
[ "com.emax.memberaccount.restapi.vo.CommonRequestDTO" ,{}]
|
- 因此,我们的程序有必要加上这个控制,即只序列化非空字段。
另外,就像我之前经常提到的,会 is one thing,会用 is another。本案也再一次敲响了警钟:在使用redis分布式缓存时,尤其控制缓存大对象,更要严禁高频访问大对象缓存。