Redis系列 | 分类树查询功能如何从2s优化到0.1s

news2024/11/22 16:12:24

大家好,今天我们继续来分享一个在项目开发过程中遇到的实际问题,这里也来梳理并总结一下我们是如何对它进行持续优化的,希望能对大家有所帮助。

分类树查询功能,在各个业务系统中可以说随处可见,特别是在一些电商系统中。

但就是这样一个看似简单的分类树查询功能,我们却优化了数次。这其中到底经历了什么呢?

背 景

我们的一个老项目使用了SpringBoot推荐的模板引擎:Thymeleaf,进行动态渲染。它是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发。它提供了一个用于整合SpringMVC的可选模块,在应用开发中,我们可以使用Thymeleaf来完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。前端开发写好Thymeleaf的模板文件,调用后端接口获取数据,进行动态绑定,就能把想要的内容展示给用户。由于当时很早这也个是从0-1的新项目,为了开快速开发功能,第一版接口是直接从数据库中查询分类数据,组装成分类树,然后返回给前端。通过这种方式,简化了数据流程,快速把整个页面功能调通了。

第1次优化

我们将该接口部署到dev环境,刚开始没啥问题。随着开发人员添加的分类越来越多,很快就暴露出性能瓶颈。我们不得不做优化了。我们第一个想到的是:加Redis缓存。流程图如下:

于是暂时这样优化了一下:

  1. 用户访问接口获取分类树时,先从Redis中查询数据。
  2. 如果Redis中有数据,则直接数据。
  3. 如果Redis中没有数据,则再从数据库中查询数据,拼接成分类树返回。
  4. 将从数据库中查到的分类树的数据,保存到Redis中,设置过期时间5分钟。
  5. 将分类树返回给用户。

我们在Redis中定义一个了key,value是一个分类树的json格式转换成了字符串,使用简单的key/value形式保存数据。经过这样优化之后,dev环境的联调和自测顺利完成了。

第2次优化

我们将这个功能部署到st环境了。刚开始测试同学没有发现什么问题,但随着后面不断地深入测试,隔一段时间就出现一次首页访问很慢的情况。于是,我们马上进行了第2次优化。我们决定使用Job定期异步更新分类树到Redis中,在系统上线之前,会先生成一份数据。当然为了保险起见,防止Redis在哪条突然挂了,之前分类树同步写入Redis的逻辑还是保留。于是,流程图改成了这样:

增加了一个job每隔5分钟执行一次,从数据库中查询分类数据,封装成分类树,更新到Redis缓存中。其他的流程保持不变。此外,Redis的过期时间之前设置的5分钟,现在要改成永久。通过这次优化之后,st环境就没有再出现过分类树查询的性能问题了。

第3次优化

测试了一段时间之后,整个网站的功能快要上线了。为了保险起见,我们需要对网站首页做一次压力测试。果然测出问题了,网站首页最大的qps是100多,最后发现是每次都从Redis获取分类树导致的网站首页的性能瓶颈。我们需要做第3次优化。该怎么优化呢?答:加内存缓存。

如果加了内存缓存,就需要考虑数据一致性问题。内存缓存是保存在服务器节点上的,不同的服务器节点更新的频率可能有点差异,这样可能会导致数据的不一致性。但分类本身是更新频率比较低的数据,对于用户来说不太敏感,即使在短时间内,用户看到的分类树有些差异,也不会对用户造成太大的影响。因此,分类树这种业务场景,是可以使用内存缓存的。于是,我们使用了Spring推荐的caffine作为内存缓存。改造后的流程图如下:

  1. 用户访问接口时改成先从本地缓存分类数查询数据。
  2. 如果本地缓存有,则直接返回。
  3. 如果本地缓存没有,则从Redis中查询数据。
  4. 如果Redis中有数据,则将数据更新到本地缓存中,然后返回数据。
  5. 如果Redis中也没有数据(说明Redis挂了),则从数据库中查询数据,更新到Redis中(万一Redis恢复了呢),然后更新到本地缓存中,返回返回数据。

需要注意的是,需要改本地缓存设置一个过期时间,这里设置的5分钟,不然的话,没办法获取新的数据。

这样优化之后,再次做网站首页的压力测试,qps提升到了500多,满足上线要求。

第4次优化

之后,这个功能顺利上线了。使用了很长一段时间没有出现问题。两年后的某一天,有用户反馈说,网站首页有点慢。

我们排查了一下原因发现,分类树的数据太多了,一次性返回了上万个分类。原来在系统上线的这两年多的时间内,运营同学在系统后台增加了很多分类。我们需要做第4次优化。这时要如何优化呢?限制分类树的数量?

答:也不太现实,目前这个业务场景就是有这么多分类,不能让用户选择不到他想要的分类吧?

这时我们想到最快的办法是开启nginxGZip功能。让数据在传输之前,先压缩一下,然后进行传输,在用户浏览器中,自动解压,将真实的分类树数据展示给用户。之前调用接口返回的分类树有1MB的大小,优化之后,接口返回的分类树的大小是100Kb,一下子缩小了10倍。这样简单的优化之后,性能提升了一些。

第5次优化

经过上面优化之后,用户很长一段时间都没有反馈性能问题。但有一天公司同事在排查Redis中大key的时候,揪出了分类树。之前的分类树使用key/value的结构保存数据的。我们不得不做第5次优化。为了优化在Redis中存储数据的大小,我们首先需要对数据进行瘦身。只保存需要用到的字段。

例如:

@AllArgsConstructor
@Data
public class Category {

    private Long id;
    private String name;
    private Long parentId;
    private Date inDate;
    private Long inUserId;
    private String inUserName;
    private List<Category> children;
}

像这个分类对象中inDate、inUserId和inUserName字段是可以不用保存的。修改自动名称。

例如:

@AllArgsConstructor
@Data
public class Category {
    /**
     * 分类编号
     */
    @JsonProperty("i")
    private Long id;

    /**
     * 分类层级
     */
    @JsonProperty("l")
    private Integer level;

    /**
     * 分类名称
     */
    @JsonProperty("n")
    private String name;

    /**
     * 父分类编号
     */
    @JsonProperty("p")
    private Long parentId;

    /**
     * 子分类列表
     */
    @JsonProperty("c")
    private List<Category> children;
}

由于在一万多条数据中,每条数据的字段名称是固定的,他们的重复率太高了。由此,可以在json序列化时,改成一个简短的名称,以便于返回更少的数据大小。这还不够,需要对存储的数据做压缩。之前在Redis中保存的key/value,其中的value是json格式的字符串。其实RedisTemplate支持,value保存byte数组。先将json字符串数据用GZip工具类压缩成byte数组,然后保存到Redis中。再获取数据时,将byte数组转换成json字符串,然后再转换成分类树。这样优化之后,保存到Redis中的分类树的数据大小,一下子减少了10倍,Redis的大key问题被解决了。

小 结

所以回过头来看,这样一个看似并不复杂的功能需求,但是要想把它做到稳定、高效、可用,一路下来还是需要考虑不少问题的。而这其中遇到的任何一个问题,一旦解决并复盘了,它也就汇聚成我们的经验了,希望这篇文章的梳理能对大家有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/702421.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UGUI无线滑动列表

在游戏开发中&#xff0c;经常会遇到需要展示大量数据的情况&#xff0c;例如排行榜、背包等。为了优化显示效果和性能&#xff0c;一个常见的做法是使用无限滑动列表&#xff08;Infinite Scroll View&#xff09;。本文将详细解析如何实现无限滑动列表。 基本原理 无限滑动列…

市电电压双向越限报警保护器电路设计

该报警保护器能在市电电压高于或低于规定值时&#xff0c;进行声光报警&#xff0c;同时自动切断电器电源&#xff0c;保护用电器不被损坏。该装置体积小、功能全、制作简单、实用性强。 一、电路工作原理 电路原理如图 3 所示。 市电电压一路由C3降压&#xff0c;DW稳压&am…

驱动开发:应用DeviceIoContro模板精讲

在笔者上一篇文章《驱动开发&#xff1a;应用DeviceIoContro开发模板》简单为大家介绍了如何使用DeviceIoContro模板快速创建一个驱动开发通信案例&#xff0c;但是该案例过于简单也无法独立加载运行&#xff0c;本章将继续延申这个知识点&#xff0c;通过封装一套标准通用模板…

一、枚举类型——新特性(switch 中的箭头语法)

支持模式匹配 你可以认为模式匹配&#xff08;pattern matching&#xff09;是在 switch 关键字上进行了显著的功能扩充。 它是分成了多个模块、 历经了 Java 的多个版本持续实现的。这保证了每个模块在其他模块加入前都可以安全地运行。最后&#xff0c;所有的模块集中到一起…

LLM大模型应用开发的本地环境搭建

尽管 ChatGPT 仍然很受欢迎&#xff0c;但泄露的 Google 内部文件表明开源社区正在迎头赶上并取得重大突破。 我们现在能够在消费级 GPU 上运行大型 LLM 模型。 因此&#xff0c;如果你是一名开发人员&#xff0c;想要在本地环境中尝试这些 LLM 并用它构建一些应用程序&#x…

Kubernetes进阶实战2

Kubernetes具有以下几个重要特性 简言之&#xff0c;Kubernetes整合并抽象了底层的硬件和系统环境等基础设施&#xff0c;对外提供了一个统一的资源池供终端用户通过API进行调用。 Kubernetes具有以下几个重要特性。 &#xff08;1&#xff09;自动装箱 构建于容器之上&#x…

定时器T0流水灯

89C52RC芯片 12Mhz&#xff1a;FC18 11.0592Mhz &#xff1a;FC67 定时器T0初值计算 12Mhz 11.0592Mhz main.c #include<regx52.h> #include<intrins.h> //_crol_循环左移函数 #include "Timer0.h" #include "Key.h" /*定时器&#xff0c;…

玩转k8s:资源管理

1 资源管理介绍 在kubernetes中&#xff0c;所有的内容都抽象为资源&#xff0c;用户需要通过操作资源来管理kubernetes。 kubernetes的本质上就是一个集群系统&#xff0c;用户可以在集群中部署各种服务&#xff0c;所谓的部署服务&#xff0c;其实就是在kubernetes集群中运行…

【C++】哈希表的改造——unordered_map和unordered_set的模拟实现

文章目录 1. unordered系列的容器封装1.1 改造1:模版参数类型的改造1.1.1 HashNode改造1.1.2 HashTable改造 1.2 改造2:迭代器的增加与封装1.2.1 迭代器类的实现1.2.2 迭代器的封装 1.3 改造3:insert的改写封装1.4 析构函数的实现1.5 unordered_map&unordered_set的封装实现…

Range_image 可视化

范围图像与点云的区别 范围图像&#xff08;Range Image&#xff09;和点云&#xff08;Point Cloud&#xff09;是两种常见的表示和处理三维点数据的方式&#xff0c;它们之间有以下区别&#xff1a; 数据结构&#xff1a;点云是一组三维点的集合&#xff0c;每个点包含位置信…

React解决setState异步带来的多次修改合一和修改后立即使用没有变化问题

我们编写这样一段代码 import React from "react" export default class App extends React.Component {constructor(props){super(props);this.state {cont: 0}}componentDidMount() {this.setState({cont: this.state.cont1})}render(){return (<div>{ thi…

Ubuntu下编译VTK

1.先安装QT&#xff0c;不知道不装行不行&#xff0c;我们项目需要。 2.去VTK官网下载VTK源码。 3.解压源码。 4.编译需要用cmake-gui&#xff0c;装QT的一般都有&#xff0c;但需要把路径添加到PATH才能用。 5.打开cmake-gui&#xff0c;设置源码路径&#xff0c;编译输出路…

项目——学生信息管理系统6

目录 权限的处理 在 MainFrm中定义 一个 权限判断的方法 在 MainFrm 的构造方法中调用一下 测试用学生身份登录 测试用教师身份登录 接下来&#xff0c;我们到学生列表页面里面 ManageStudentFrm&#xff0c;继续权限的设置&#xff0c;学生只能查看自己的信息&#xff0c…

神坑:ElasticSearch8集群启动报错“Device or resource busy”(Docker方式)

昨天在Docker中配置ElasticSearcch8集群模式时&#xff0c;先初步配置了master主节点。然后主节点启动就报错&#xff0c;看日志&#xff0c;提示“Device or resource busy”。异常第一句大概这个样子&#xff1a; Exception in thread "main" java.nio.file.FileS…

WMS 窗口属性

WMS 窗口属性 1、窗口类型与层级1.1 Application Window普通应用程序窗口1.2 Sub Window子窗口1.3 System Window系统窗口 2、层级值与窗口类型2.1 WindowState2.2 WindowManagerPolicy 窗口管理的策略机制2.3 WindowToken句柄 3、窗口属性 LayoutParams Activity的预览窗口Sta…

基于51单片机的秒表系统

目录 基于51单片机的秒表系统一、原理图二、部分代码三、视频演示 基于51单片机的秒表系统 一、原理图 二、部分代码 #include <reg52.h>#define duanxuan P2 #define weixuan P1unsigned char code wxcode[]{0X01, 0x02, 0x04, 0x08, 0X10, 0X20, 0X40, 0X80};…

C# WPF 路径动画

路径动画&#xff1a;一个东西沿着你画的的线跑。 微软对这个有很详细的说明&#xff0c;有需要请参照微软Learn网站 cs的代码 PathGeometry pathGeometry new PathGeometry();PathFigure pathFigure new PathFigure();//pathFigure.IsClosed true;pathFigure.StartPoint…

试着攻击自己写的网站

背景介绍 概念简介 CSRF XSS攻击是一种常见的安全攻击&#xff0c;它通过伪造用户输入&#xff0c;利用CSRF漏洞获取用户的敏感信息或者执行恶意操作。CSRF XSS攻击不仅会给用户带来损失&#xff0c;还会对互联网安全造成威胁。因此&#xff0c;防范CSRF XSS攻击已经成为了当…

C++ - 20230629

一. 思维导图 二. 练习 #include <iostream> using namespace std;class Person { private:int age;int *p; public://无参构造Person():p(new int(89)){age 18;}//有参构造Person(int age,int num){this->age age;this->pnew int(num);}//拷贝构造函数Person(P…