不扒瞎,这个程序让我从150s优化到了5s

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 在优化一个业务开发组的生产问题时,发现销售管理系统查询数据延迟高达2-3分钟。问题根源在于,程序在for循环中频繁读取Redis大KEY数据,导致性能下降。解决方案是采用本地缓存HutoolCache,将耗时降至毫秒级别。此外,还对RedisTemplate配置进行了研究,Jackson2JsonRedisSerializer在序列化时包括了所有字段,即使字段值为null,增加了数据体积。通过对ObjectMapper的调整,仅序列化非空字段,可以显著提升redis读取性能。本文同时还提醒我们在使用Redis时要注意大对象缓存,强调了正确使用和配置缓存以及避免大对象存储的重要性。

前天晚上加班完成部门KPI考核计划后,看到业务开发组的几个小伙伴在处理生产问题。我上前了解情况。

 

产品经理反馈,销售主管登陆销管系统查询数据时,非常慢,慢到2~3分钟。

销管系统,客户交易明细页面,查询客户交易数据的逻辑是:调用远程数据中心的RPC接口,拿到原始交易数据记录的集合,然后在本地程序来给各数据记录的客户名称、服务商名称、销售人员名称、所属部门、上级销售主管赋值。

 

 


这里有一个事实是,数据中心的交易表,是一个拥有15,000,000条存量数据的大宽表,查询这些交易数据通常比较耗时。但是,总不至于2~3分钟这么慢!

查看日志,发现程序处理耗时动辄高达150s。

150s是个庞大到骇人听闻的数字!

通过分析,其中,获取远程交易数据耗时≈3s,这个耗时倒是在150s里占了个零头。本地内存数据匹配竟然耗时140多秒,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条数据记录的集合数据的大小为150KB,显然,存储到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级了。整体方法耗时也控制在了5s以内。


那么,回过头来分析,我们看程序里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分布式缓存时,尤其控制缓存大对象,更要严禁高频访问大对象。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
算法 程序员 编译器
当程序遇上困难:程序调试的艺术(VS)
当程序遇上困难:程序调试的艺术(VS)
68 0
|
Python
上古代码漫游记(二):把陷阱去掉了,反倒踩进了新的陷阱?
上古代码漫游记(二):把陷阱去掉了,反倒踩进了新的陷阱?
107 0
|
安全 Java 数据库连接
麻了,代码改成多线程,竟有9大问题 下
麻了,代码改成多线程,竟有9大问题 下
|
消息中间件 JavaScript 小程序
麻了,代码改成多线程,竟有9大问题 上
麻了,代码改成多线程,竟有9大问题 上
|
Java C语言
看似无害的代码如何搞垮系统
编程就像魔法。最近遇到一个诡异的问题:添加一段看似无害的简单代码后,系统原有功能不可用了。 ## 复现演示 jdk 8 可使用如下演示代码复现这个问题。 `TaskCenter` 是一个任务框架,可添加多个任务,随后框架将执行这些任务。 `First` 任务是新增代码,看起来简单无害,且看不出对原有任务 `Count` 有何影响,但添加 `First` 任务后,其自身执行正常,原本正常的 `C
127 0
|
数据采集 JSON 小程序
学小程序还不懂代码结构?——每天三分钟玩转小程序2
学小程序还不懂代码结构?——每天三分钟玩转小程序2
学小程序还不懂代码结构?——每天三分钟玩转小程序2
|
开发框架 Java 测试技术
【测试基础】五、这样提bug单,开发小哥还会怼你么?
【测试基础】五、这样提bug单,开发小哥还会怼你么?
【测试基础】五、这样提bug单,开发小哥还会怼你么?
|
Python
又烧脑又炫技还没什么用,在代码里面打印自身
又烧脑又炫技还没什么用,在代码里面打印自身
207 0
又烧脑又炫技还没什么用,在代码里面打印自身
|
自然语言处理 编译器 C语言
预处理和程序的编程(跑路人笔记1)
预处理和程序的编程(跑路人笔记)
预处理和程序的编程(跑路人笔记1)
|
存储 自然语言处理 程序员
预处理和程序的编程(跑路人笔记2)
预处理和程序的编程(跑路人笔记)
预处理和程序的编程(跑路人笔记2)