不扒瞎,这个程序让我从300s优化到了10s
【简介】在优化一个业务开发组的生产问题时,发现销售管理系统查询数据延迟高达2-3分钟。问题根源在于,程序在for循环中频繁读取Redis大KEY数据,导致性能下降。解决方案是采用本地缓存HutoolCache,将耗时降至毫秒级别。此外,还对RedisTemplate配置进行了研究,Jackson2JsonRedisSerializer在序列化时包括了所有字段,即使字段值为null,增加了数据体积。通过对ObjectMapper的调整,仅序列化非空字段,可以显著提升redis读取性能。本文同时还提醒我们在使用Redis时要注意大对象缓存,强调了正确使用和配置缓存以及避免大对象存储的重要性。
前天晚上加班完成部门Q4KPI考核计划后,看到业务开发组的几个小伙伴在处理生产问题。我上前了解情况。
产品经理反馈,销售主管登陆系统查询数据时,非常慢,慢到4~5分钟。
销管系统,客户交易明细页面,查询客户交易数据的逻辑是:调用远程数据中心接口,拿到原始交易数据记录的集合,然后在本地程序来给各数据记录的客户名称、服务商名称、销售人员名称、所属部门、上级销售主管赋值。
这里有一个事实是,数据中心的交易表,是一个拥有15,000,000条存量数据的大宽表,查询这些交易数据通常比较耗时。但是,总不至于4~5分钟这么慢!
查看日志,发现程序处理耗时动辄高达300s。
300s是个庞大到骇人听闻的数字!
通过分析,其中,获取远程交易数据耗时≈6s,这个耗时倒是在300s里占了个零头。本地内存数据匹配竟然耗时200多秒,incredible!unbelievable!
当务之急,是看程序能不能在5s以内响应给前端页面。
那接下来要对各个匹配数据的程序段来分析。通过细化耗时,发现在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#getCache 封装了Redis的get/set操作。
EmaxSalerMapper#selectSaleDepartRelation 是查本地数据库获取销售关系,共有223条数据,耗时6~7ms。
CommonRequestDTO是一个POJO模型类。
那么,这段代码似乎也看不出哪里慢呀!
仔细一分析,发现端倪。Cc同学怀疑问题出在读Redis上。果不其然, 这个拥有223条数据记录的集合数据的大小为113KB!显然,存储到Redis里就构成了大KEY。大KEY可能会导致Redis存储倾斜的问题。而且呢,这段程序在for循环中频繁调用Redis来获取这个大KEY的值,性能必然拉跨。
当务之急,最快的解决办法,是用本地缓存来搞,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以内。
那么,回过头来分析,我们看程序里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个属性,从数据库里查出来的223条数据,只用到了其中的5个属性,可见序列化null字段后,数据体积无形中增大很多。通过下面对ObjectMapper的测试代码来比较一下,很明显可以看到单个对象序列化后在数据量方面的差异:
@Test public void testObjectMapper() throws JsonProcessingException { ObjectMapper om; om = new ObjectMapper(); om.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY) .enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); System.out.println("序列化所有字段(无论这些字段是否有值) ↓ ↓ ↓"); 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" ,{}] |
- 因此,我们程序里的RedisTemplate配置有必要改一下,只序列化非空字段。
另外,就像我之前经常提到的,会 is one thing,会用 is another。本案也再一次敲响了警钟:在使用Redis分布式缓存时,尤其控制缓存大对象,更要严禁高频访问大对象。
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/16744494.html