写点什么

分类树,我从 2s 优化到 0.1s

作者:Java你猿哥
  • 2023-05-15
    湖南
  • 本文字数:2595 字

    阅读完需:约 9 分钟

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


但就是这样一个简单的分类树查询功能,我们却优化了 5 次。

到底是怎么回事呢?

背景

我们的网站使用了 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 次优化。

这时要如何优化呢?

限制分类树的数量?

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

这时我们想到最快的办法是开启 nginx 的 GZip 功能。

让数据在传输之前,先压缩一下,然后进行传输,在用户浏览器中,自动解压,将真实的分类树数据展示给用户。

之前调用接口返回的分类树有 1MB 的大小,优化之后,接口返回的分类树的大小是 100Kb,一下子缩小了 10 倍。

这样简单的优化之后,性能提升了一些。

第 5 次优化

经过上面优化之后,用户很长一段时间都没有反馈性能问题。

但有一天公司同事在排查 Redis 中大 key 的时候,揪出了分类树。之前的分类树使用 key/value 的结构保存数据的。

我们不得不做第 5 次优化。

为了优化在 Redis 中存储数据的大小,我们首先需要对数据进行瘦身。

只保存需要用到的字段。

例如:

@AllArgsConstructor@Datapublic 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@Datapublic 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 问题被解决了。


用户头像

Java你猿哥

关注

一只在编程路上渐行渐远的程序猿 2023-03-09 加入

关注我,了解更多Java、架构、Spring等知识

评论

发布
暂无评论
分类树,我从2s优化到0.1s_Java_Java你猿哥_InfoQ写作社区