写点什么

前端菜鸟让老接口提速 60% 的原理与实现

  • 2020-11-28
  • 本文字数:3988 字

    阅读完需:约 13 分钟

前端菜鸟让老接口提速60%的原理与实现

导语 | 年久失修的老接口堪称所有程序员们的噩梦,它们逻辑复杂、严重卡顿、无人维护,令经手的开发头痛不已。本文将为大家分享通过 nodejs + graphQL + redis + schedule 技术组合对老接口进行优化提速,提升前端体验的原理与实践,希望与大家一同交流。文章作者:艾瑞坤,腾讯前端研发工程师。


一、背景


最近在维护一个老项目的时候,发现页面严重卡顿,页面长时间展示“加载等待中”。经过分析发现有一个老接口调用延时非常高,平均调用时间在 3s 以上。



每次在加载页面和翻页时都会停顿很久,严重影响体验。老接口服务存在以下几个问题:


  • 太多无效数据 :接口返回数组的每条数据都包含了上百个字段,而前端展示只使用了其中 10 字段,太多的无效数据占据了接口传输时间。

  • 接口调用链过长 :接口存在复杂逻辑,并且老接口内部还调用了其他 n 个接口的服务,导致前端调用接口延时过长。

  • 代码年久失修 :老接口服务没人维护,无人知道如何修改和部署,没有文档,调用全靠猜。


作为一个前端工程师,如何在不修改老接口代码的情况下去优化这个接口延时过长的 case 呢?


笔者决定做一个 node 代理层,用下面三个方法进行优化:


  • 按需加载 -> graphQL :通过描述接口协议字段的结构,然后配置指定规则 schema,对数据进行字段的按需加载。

  • 数据缓存 -> redis :用 redis 来对老接口服务返回的数据进行缓存,让用户请求绕过老接口的复杂逻辑,直接获取数据。

  • 轮询更新 -> schedule :用 node-schedule 定时更新数据缓存,保证用户每次请求获取最新数据。


整体架构如下图所示:



二、按需加载 graphQL


由于前端需要绘制一个图表,我们每次请求接口都要返回 1000 多条数据,返回的数组中,每一条数据都有上百个字段,其实我们前端只用到其中的 10 个字段进行展示和绘制图表。


如何从一百多个字段中,抽取任意 n 个字段,这就用到 graphQL。graphQL 按需加载数据只需要三步:


  • 定义数据池 root;

  • 描述数据池中数据结构 schema;

  • 自定义查询数据 query。


1. 定义数据池 root


由于原业务逻辑和接口协议比较复杂,没法一一在文中叙述。为了方便理解,我用“屌丝追求女神”的场景来说明 graphQL 按需加载字段的实现。


首先我们定义一个女神 girls 数据池,里面包含女神的所有信息,如下:


// 数据池var root = {    girls: [{        id: 1,        name: '女神一',        iphone: 12345678910,        weixin: 'xixixixi',        height: 175,        school: '剑桥大学',        wheel: [{ name: '备胎1号', money: '24万元' }, { name: '备胎2号', money: '26万元' }]    },    {        id: 2,        name: '女神二',        iphone: 12345678910,        weixin: 'hahahahah',        height: 168,        school: '哈佛大学',        wheel: [{ name: '备胎3号', money: '80万元' }, { name: '备胎4号', money: '200万元' }]    }]}
复制代码


数据池包含了两个女神的所有信息,包括女神的名字(name)、手机(iphone)、微信(weixin)、身高(height)、学校(school)、备胎们的信息(wheel)。接下来我们再对这些数据结构进行描述。


2. 描述数据池中数据结构 schema


const { buildSchema } = require('graphql');
// 描述数据结构 schemavar schema = buildSchema(` type Wheel { name: String, money: String } type Info { id: Int name: String iphone: Int weixin: String height: Int school: String wheel: [Wheel] } type Query { girls: [Info] }`);
复制代码


上面这段代码就是女神信息的 schema,schema 其实就是将女神的信息进行结构化,经过结构化的数据,才可以进行数据按需获取。


在 nodejs 中使用 graphql 这个库,里面包含了 graphQL 操作字段的所有 api。我们用 buildSchema 这个方法来构建女神信息的 schema。


那么如何描述女神信息的 schema 呢?首先我们用 type Query 定义了一个对女神信息的查询,里面包含了很多女孩 girls 的信息 Info,这些信息是一堆数组,所以是[Info]。


我们在 type Info 中描述了一个女孩的所有信息的维度,包括名字(name)、手机(iphone)、微信(weixin)、身高(height)、学校(school)、备胎集合(wheel)。


数据类型主要是 String 和 Int,如果出现了嵌套对象类型,就参考备胎(wheel)的定义方式,单独用 type 定义一个 Wheel 备胎类型,这样就可以进行结构化的复用类型了。


3. 定义查询规则 query


得到女神的信息描述(schema)后,就可以自定义各种组合,获取女神的信息了。比如我想和女神认识,只需要拿到她的名字(name)和微信号(weixin)。查询规则代码如下:


const { graphql } = require('graphql');
// 定义查询内容const query = ` { girls { name weixin } }`;
// 查询数据const result = await graphql(schema, query, root);
复制代码


对女神的名字、微信构造了一个 query 查询,注意这个语法不是我们前端的 json 语法,是 graphQL 特定的语法。


查询的时候,我们使用 graphql 这个库里面的 graphql 方法,将女神信息描述 schema、女神数据池 root、查询语句 query 一并传入 graphql 方法,这样就可以对数据进行按需加载了。


筛选结果如下:



我们按需获取到了女神的名字、微信,剔除女神了其他不需要的信息手机、身高、学校、备胎,这就是 graphQL 的核心思想:按需加载数据。


又比如我想进一步和女神发展,我需要拿到她备胎信息,查询一下她备胎们(wheel)的家产(money)分别是多少,分析一下自己能不能获取优先择偶权。查询规则代码如下:


const { graphql } = require('graphql');
// 定义查询内容const query = ` { girls { name wheel { money } } }`;
// 查询数据const result = await graphql(schema, query, root);
复制代码


这个例子我们涉及到了一个嵌套查询,把女神名下所有备胎的 money 全查了出来


筛选结果如下:



我们通过女神的例子,展现了如何通过 graphQL 按需加载数据。映射到我们业务具体场景中:老接口返回的每条数据都包含 100 个字段,我们配置 schema,获取其中的 10 个字段,这样就避免了剩下 90 个不必要字段的传输。


graphQL 还有另一个好处就是可以灵活配置。这个接口需要 10 个字段,另一个接口要 5 个字段,第 n 个接口需要另外 x 个字段,按照传统的做法我们要做出 n 个接口才能满足,现在只需要一个接口配置不同 query 就能满足所有情况了。



三、缓存 redis


第二个优化手段,使用 redis 缓存。老接口内部还调用了多个其他第三方接口,极其耗时耗资源。我们用 redis 来缓存老接口的聚合数据,下次再调用老接口,直接从缓存中获取数据即可,避免高耗时的复杂调用,简化后代码如下:


const redis = require("redis");const { promisify } = require("util");
// 链接redis服务const client = redis.createClient(6379, '127.0.0.1');
// promise化redis方法,以便用async/awaitconst getAsync = promisify(client.get).bind(client);const setAsync = promisify(client.set).bind(client);
async function list() { // 先获取缓存中数据,没有缓存就去拉取天秀接口 let result = await getAsync("缓存"); if (!result) { // 拉接口 const data = await 老接口(); result = data; // 设置缓存数据 await setAsync("缓存", data) } return result;}
list();
复制代码


我们用 redis 的 npm 包来进行缓存相关的操作,redis 类似咱们的数据库,开始的时候先用 redis.createClient 建立连接。


由于 redis 提供的方法都是异步回调的函数,所以我们用 promisify 给所有函数包一下让我们能用 async/await 进行同步调用。


每次接口调用的时候,我们先通过 getAsync 来读取 redis 缓存中的数据,如果有数据,直接返回,绕过老接口复杂调用。


如果没有数据,就调用老接口,用 setAsync 将老接口返回的数据存入缓存中,以便下次调用。主体流程如下图所示:



因为 redis 存储的是字符串,所以在设置缓存的时候,需要加上 JSON.stringify(data)。


将数据放在 redis 缓存里有几个好处,可以实现多接口复用、多机共享缓存等。


四、轮询更新 schedule


最后一个优化手段:轮询更新 -> schedule。


数据源一直在变化,会导致缓存的数据与数据源不一致,需要定时更新。更新的方法有很多种,听专业的后端小伙伴说有分段式数据缓存、主从同步读写分离、高并发同步策略等等。


由于我不是专业的后端人员,并且老接口调用量不大,对应的数据源更新频率低。所以我用了最简单的轮询更新策略。代码如下:


const schedule = require('node-schedule');
// 每个小时更新一次缓存schedule.scheduleJob('* * 0 * * *', async () => { const data = await 天秀接口(); // 设置redis缓存数据 await setAsync("缓存", data)});
复制代码


用 node-schedule 这个库来进行定时轮询更新缓存,设置轮询间隔为* * 0 * * *,这句代码的意思就是设置每个小时的第 0 分钟就开始执行缓存更新逻辑,将获取到的数据更新到缓存中。


这样每当前端在调用接口的时候,就能获取到最新数据,避免了直接调用老接口,直接将缓存中的数据取出并快速返回前端。这就是 redis 缓存和轮询更新的好处。


五、结语


经过以上三个方法优化后,接口请求耗时从 3s 降到了 860ms,用户体验得到了显著的提升。



这些代码都是从业务中简化后的逻辑,真实的线上 ToC 业务场景远比这要复杂:分段式数据存储、主从同步 读写分离、高并发同步策略等等。


每一块技术点都需要专研和实践,由于笔者是前端开发,对后端知识和技术理解有限,如有什么说的不对和不完善的地方,欢迎在评论区与我交流。


参考资料


[1] 本文项目 github 地址:


https://github.com/airuikun/blog/tree/master/src/graphql%2Bredis


本文转载自公众号云加社区(ID:QcloudCommunity)。


原文链接


前端菜鸟让老接口提速60%的原理与实现


2020-11-28 16:251707

评论

发布
暂无评论
发现更多内容

Java的Io模型你了解多少?尚硅谷大厂学院课,Java开发面试笔试题大汇总

Java 程序员 后端

Java程序员必会!开课吧java高级架构师课程,Java开发大厂面试经验

Java 程序员 后端

Java知识体系!java黑马视频和达内,链表反转的两种实现方法

Java 程序员 后端

Java程序员面试中最容易答错的8道面试题,tomcat面试题及答案

Java 程序员 后端

Java编程入门经典,linux使用教程课后答案,mysql常见笔试题

Java 程序员 后端

Java架构师必备技能,java程序设计实用教程第五版答案,掌握这个提升路径

Java 程序员 后端

Java编程入门自学,牛客网在线编程,Java基础入门视频教程

Java 程序员 后端

Java知识体系!极客学院黑马程序员,BIO和NIO有啥区别

Java 程序员 后端

Java程序员最新职业规划,尚学堂高琪300集,初级Java工程师面试题

Java 程序员 后端

Java爬虫爬取视频,尚硅谷笔试答案,最全面试考点与面试技巧

Java 程序员 后端

Java百度云教程,深入java虚拟机百度云,附详细答案

Java 程序员 后端

Java算法基础面试题,java教程张孝祥百度云,Java初级程序员面试题目

Java 程序员 后端

Java日常开发的12个坑,你踩过几个,一招让你拿下seata分布式事务框架

Java 程序员 后端

Java程序员如何有效提升学习效率,如何化身BAT面试收割机

Java 程序员 后端

Java程序员面试笔试真题,java零基础入门视频百度云,阿里P7大牛亲自讲解

Java 程序员 后端

Java春招实习面试经验汇总,图灵学院诸葛,Java微服务架构视频下载

Java 程序员 后端

Java架构师进阶之路,马士兵的java教程,大厂Java面试总结+详细解答

Java 程序员 后端

Java百度云资源,java基础案例教程黑马程序员在线阅读,美团Java面试流程

Java 程序员 后端

Java百度云,springboot实例教程,面试大厂应该注意哪些问题

Java 程序员 后端

Java研发岗必问30+道高级面试题,腾讯,字节等大厂面试真题汇总

Java 程序员 后端

Java研发岗面试复盘总,4面技术5面HR附加笔试面

Java 程序员 后端

Java程序员面试笔记,极客时间vue开发实战,Java进阶教程视频

Java 程序员 后端

Java笔试题及答案详解,nginx入门到精通百度云,全网最全原理讲解

Java 程序员 后端

Java编程书籍推荐,尚硅谷springboot,遇到的面试官都是架构师级别

Java 程序员 后端

Java月薪过万要掌握的技能,javajdk下载教程,高级Java工程师面试问题

Java 程序员 后端

Java框架,黑马java视频教程,面试资料分享

Java 程序员 后端

云栖发布|企业级互联网架构全新升级 ,助力数字创新

阿里巴巴云原生

阿里云 云原生 产品升级 云栖大会

Java程序员全套,百度三面牛客网猿生活,疯狂膜拜

Java 程序员 后端

Java笔试编程题大全带答案,mysql入门视频教程,Java多态实现原理

Java 程序员 后端

Java经典入门教程,vue尚学堂,Java面试问项目

Java 程序员 后端

Java编程入门经典,慕课网java架构师百度网盘,字节跳动Java高级工程师

Java 程序员 后端

前端菜鸟让老接口提速60%的原理与实现_大前端_云加社区_InfoQ精选文章