如何做好一个基础的搜索功能?记一个因客户大数据量而导致的后发先至Bug
壹 ❀ 引
上篇文章算是开了一个新系列,因为工作缘故,我基本每天都在跟各式各样的bug
打交道。其实站在一个开发的角度,我想每个人应该都更喜欢创造新代码,创造新bug
,而不是每天都泡在茫茫代码海洋中定位和修复问题。
当然,当产品部资源不够时,我偶尔也会接手做做需求,比如上周有个比较急的需求没资源投入,产品管理那边就从几个组都抽出了一个人凑成了一个临时小组,前端这边工作就是由我来负责了。当我做完前端方案去评审的时候,技术委员会的大佬(入职的比较晚)对我发出灵魂拷问,你也做需求??
所以有时候真的需要对这份工作调整好心态,以前我也经常问自己,做前端不做需求写大量的代码,这真能学到东西吗,因此我也跟我所在的部门负责人抱怨过这件事,他对我说,你在一家公司从来就没有人能限制你学到什么。
我当时突然想起了一部看过的电视剧东京大饭店
(我基本不看电视剧,因为没什么耐心...但这个是真好看= =),影片讲述了男主为打造世界最棒的米其林三星餐厅找回曾经的餐厅伙伴一起追梦的故事,而开店前期缺人因为机缘巧合招了个菜鸟新人(除了这个新人其余的人都算顶级厨师了)。男主与其他人苦思冥想研究新品菜系,新人不是在训练切5mm的萝卜丁就是在切5mm萝卜丁,每次想要得到表现的机会去做菜,一次次被拒绝,也因此觉得自己在这学不到东西得不到认可,差点跟男主闹翻。我当时就想,不是吧大哥,你深处一个顶级厨师的团队,身边都是世界级的大佬,哪怕不做菜,天天在这个氛围中花点心思学一学,出去都能吊打各种厨师了!!这么难得的经历,完全就是死脑筋!!
正所谓当局者迷旁观者清,我自己何尝不像是这个电视剧的菜鸟新人呢?项目代码就放在那,想学到什么想了解什么,完全是由自己的心来决定的,除了自己没有人能限制你学到什么。
所以后来我回顾自己经手的一些bug
,才发现确实有很多值得研究和深思的问题(技术委员会大佬也觉得很多问题是值得搜集起来讲一讲),这也是为什么我开始写这类修复经历的原因。
那么东京大饭店
的菜鸟新人最后到底有没有得到男主认可,男主一行人有没有追梦成功呢?本文肯定也不会提,而我也在接受当下,一步步改写自己的"命运"。
另外,本文所阐述的bug
并不是专属于react
,而是一个很常见的前端搜索场景问题,所以即便不会react
我也推荐了解下,万一面试被问场景问题遇到了呢?好了,闲话说了一堆,本文正式开始。
贰 ❀ 问题场景再现
在上周csm
就给我提了一个客户反馈的bug
单,因为这个客户是一个大客户,所以反馈的bug
我们都比较看重。但因为上周确实比较忙一直没时间跟进,结果本周一我刚出地铁还在路上,csm
企业微信就滴滴我了,问我今天有没有时间远程下这个问题,想与我同步下方便约客户。
我其实之前也尝试复现过这个问题,但不管是我本地环境还是客户侧私有部署测试环境,都没能复现这个问题。所以我跟他说,上午给我点时间先熟悉下这块功能的代码,下午2点就可以远程。
远程其实就是远程链接客户的电脑,操作客户的电脑来排查问题,一般只有我们这边实在复现不了问题时才会提出远程。远程的环境因为都是正式环境,随便一个文件打开都是十几万行高度压缩的代码,可以说毫无阅读体验,不提前做下准备到时候远程了找不出原因,那我不得尴尬死。
等到下午远程链接了客户也是一番操作....对方演示就是能复现,我接管鼠标操作就是不能复现,几番摸索,终于还是在客户电脑上复现了这个问题,因为客户数据安全的问题,这里就不能直接贴问题原图了,但我们还是先来还原下问题场景。
bug
现象其实很简单,在成员页用户可以通过搜索查找到符合条件的所有成员,然后可以勾选成员加入到当前项目。所以每次输入或者修改输入框内容,一定都会发起请求,然后前端响应让展示区域的列表发生改变。
比如上图中,一开始有个A
,那么一开始内容区域展示的都是A
相关的成员。紧接着用户做了一次清空操作,然后又输入了一个B
,理论上来说,最终内容区域应该展示B
相关的人员,但很遗憾,B
相关的成员只展示了一会,紧接着区域的数据又变成了未加任何搜索字段的全部数据,也就是初始数据了。所以客户提单说,搜索输入框的结果每次都只能展示一会,过会自己就变了。
我在复现问题后,打开了客户电脑控制台的Network
,看了眼复现操作中的请求调用,问题的原因马上就清楚了。我用一个图来表示这个过程。
这个行为中一共发起了两次请求,在我远程查看了两次请求耗时发现,第一次请求耗时了4s,第二次耗时只用了1S。也就是说这就是个后发先至
的问题,后发的请求B
先回来了,因此前端先展示了B
的内容,没多久,第一次请求也就是空条件的结果又回来了,覆盖了B
的请求结果,这就是这个问题的根因。
而后端对于这种请求处理都是并发
的,相互之间并不会有所感知,你前端发几次请求过来,肯定是先处理完的我先反馈给你,如果后端加个队列严格按先后顺序处理,那要是炸一个接口半天没响应,其余的请求都没法玩了。
而为什么我自己没能复现这个问题呢?这是因为客户侧数据有几十万,而我自己的测试账号一共就3条user
数据....没有这个数据量支撑,复现几率就是0。
同时我又想到了第二个问题,为什么没有筛选条件的请求反而比有筛选条件的慢?理论上来说我不传筛选条件,你都不用查了,直接给数据我,这边得到的结论是,后端那边的查询是越接近底层查询越快,越接近业务层查询速度越慢,当筛选条件为空时,因为没筛选,所以后端业务层面对的是几十万的数据量;而有了筛选条件,到业务层的数据已经被筛选过一次可能就只有几百条了,用时反而更少了。
那怎么解决呢?让后端解决?后端也明说了,数据量摆在那,接口又是并发不可能给你排队处理,所以问题还是得前端来解决,下面说说方案。
叁 ❀ 解决方案
让我们回到问题本身,一个看似简单的搜索居然能引发这样的有趣问题,假设这是个面试题又该如何解决呢?其实思路可分为两步。
我一开始看这个问题,怀疑是滥用防抖造成的,结果一看代码,好家伙根本没用防抖= =,也就是说假设用户是光速A-空-B
,那确实会发起两个请求,第一次肯定是给这种高频修改加一个防抖,无意义的请求能不发就不发。
但事实上防抖并没有从根本解决问题,问题的根因是数据量太大,查询确实要那么久,我们设置防抖一般也就是300ms
左右,假设用户A-空-B
的间隔超过了你设置的防抖时间,前端还是会发起两次请求,而后端还是会有后发先至
的可能性,所以单一个防抖解决不了问题。而这个时候,我们还需要加一步操作,那就是加个开关去取消上一次的请求,画个图:
(PS:防抖还是要加,假设现在数十万用户同时访问,不加防抖造成的无意义请求那就是数十万个了,还是会造成服务器资源大量浪费)
我们来解释下这个过程,一开始有个请求开关,默认值是false
。
模拟一次请求:请求发起-->请求开关默认是false
-->发起请求-->修改请求开关为true
-->请求结束-->修改请求开关为false
-->结束。
模拟上面的bug
场景:请求发起-->开关是false
-->发起请求-->修改请求开关为true
-->请求还没结束又发起了第二次请求-->请求开关是true
-->取消上次请求-->继续走正常请求路线...(假设过程中又操作了多次继续重复取消操作)...-->结束。
大家可以思考下这个过程,对于同一请求,如果用户确实操作了多次,对于用户而言TA关心的其实就是最后一次操作的结果,因此当前面一次请求没回来,我们完全可以舍弃掉这次请求,直接发起新的请求,后续操作同理。
当然,假设接口响应巨快,快到超出了用户操作间隔,那我们其实啥也不用干,毕竟后端返回数据先后顺序完全符合用户预期,requestSwitch
开发自然会被合理切换,咱也不用做额外处理。
有同学可能要问了,你说的我都懂,那这个请求取消我该怎么做,其实axios
就有提供一个API
叫CancelToken
,这就是解决上述问题的妙药,而取消的底层原理与XMLHttpRequest.abort()有很大关系。因为篇幅问题,关于取消原理还是另起一篇文章来介绍吧。
这篇文章其实说到这也没贴一点代码,因为前面也说了,这个问题并不属于react
专属的问题,而是每一个搜索面对大数据量时都可能遇到的问题,重要的是方案,有了方案看看axios
文档我还不信你还做不出来。
肆 ❀ 总
OK,那么到这里又介绍了一个有趣的bug
排查经历。我想搜索功能大家应该都做过,但不一定都有遇到过这种场景问题,比如我前面前端三年还真没处理过此类问题,毕竟项目太简单了,这也是为什么我要写这类博客的原因。bug
永远有的修,所以这类文章应该还会更新很多篇,也算是一个小科普了,下一个bug
已经在安排中了,那么本文就到这里了!