但便是这样一个大略的分类树查询功能,我们却优化了5次。
到底是怎么回事呢?
背景我们的网站利用了SpringBoot推举的模板引擎:Thymeleaf,进行动态渲染。
它是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的运用开拓。
它供应了一个用于整合SpringMVC的可选模块,在运用开拓中,我们可以利用Thymeleaf来完备代替JSP或其他模板引擎,如Velocity\FreeMarker等。
前端开拓写好Thymeleaf的模板文件,调用后端接口获取数据,进行动态绑定,就能把想要的内容展示给用户。
由于当时这个是从0-1的新项目,为了开快速开拓功能,我们初版接口,直接从数据库中查询分类数据,组装身分类树,然后返回给前端。
通过这种办法,简化了数据流程,快速把全体页面功能调通了。
第1次优化我们将该接口支配到dev环境,刚开始没啥问题。
随着开拓职员添加的分类越来越多,很快就暴露出性能瓶颈。
我们不得不做优化了。
我们第一个想到的是:加Redis缓存。
流程图如下:
于是暂时这样优化了一下:
用户访问接口获取分类树时,先从Redis中查询数据。如果Redis中有数据,则直接数据。如果Redis中没有数据,则再从数据库中查询数据,拼接身分类树返回。将从数据库中查到的分类树的数据,保存到Redis中,设置过期韶光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作为内存缓存。
改造后的流程图如下:
用户访问接口时改成先从本地缓存分类数查询数据。如果本地缓存有,则直接返回。如果本地缓存没有,则从Redis中查询数据。如果Redis中有数据,则将数据更新到本地缓存中,然后返回数据。如果Redis中也没有数据(解释Redis挂了),则从数据库中查询数据,更新到Redis中(万一Redis规复了呢),然后更新到本地缓存中,返回返回数据。
须要把稳的是,须要改本地缓存设置一个过期韶光,这里设置的5分钟,不然的话,没办法获取新的数据。
这样优化之后,再次做网站首页的压力测试,qps提升到了500多,知足上线哀求。
第4次优化之后,这个功能顺利上线了。
利用了很长一段韶光没有涌现问题。
两年后的某一天,有用户反馈说,网站首页有点慢。
我们排查了一下缘故原由创造,分类树的数据太多了,一次性返回了上万个分类。
原来在系统上线的这两年多的韶光内,运营同学在系统后台增加了很多分类。
我们须要做第4次优化。
这时要如何优化呢?
限定分类树的数量?
答:也不太现实,目前这个业务场景便是有这么多分类,不能让用户选择不到他想要的分类吧?
这时我们想到最快的办法是开启nginx的GZip功能。
让数据在传输之前,先压缩一下,然后进行传输,在用户浏览器中,自动解压,将真实的分类树数据展示给用户。
之前调用接口返回的分类树有1MB的大小,优化之后,接口返回的分类树的大小是100Kb,一下子缩小了10倍。
这样大略的优化之后,性能提升了一些。
第5次优化经由上面优化之后,用户很长一段韶光都没有反馈性能问题。
但有一天公司同事在排查Redis中大key的时候,揪出了分类树。之前的分类树利用key/value的构造保存数据的。
我们不得不做第5次优化。
为了优化在Redis中存储数据的大小,我们首先须要对数据进行瘦身。
只保存须要用到的字段。
例如:
java复制代码@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字段是可以不用保存的。
修正自动名称。
例如:
java复制代码@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问题被办理了。
作者:苏三说技能链接:https://juejin.cn/post/7233012756315963452