谨慎使用全局变量

背景

之所以写这篇文章,是因为有同事使用全局变量不当导致了bug。所以在解释标题之前,首先说一下业务背景。

很简单,就是有一个页面可以办理某个业务,这个业务又分为两种类型,可以随意切换类型。发现问题的过程是,页面初始化时默认是A类型,所以此时前端会按照A类型传参调用后台大概3个接口,我们暂且称作接口1,接口2和接口3吧。其中接口3的请求参数依赖接口1和接口2的响应参数,接口1和接口2的返回数据会展示到前端,然后调用接口3时将从接口1和接口2的返回参数中拿数据传递给接口3,然后将接口3返回的数据展示,到此页面初始化加载完成。

由下面页面草图可以看出,接口1,2,3都依赖于类型来完成对应的逻辑处理,在接口调用上肯定是先调接口1,2(二者没有先后顺序),然后调接口3。之后在从A类型切换至B类型时又会重新按B类型重新加载一遍接口1,2,3,展示B类型对应的数据。

问题排查

大概的业务规则就是这样的,很简单。但是在测试中发现,当页面初始化时,迅速切换到B类型,前端弹出一个错误窗口“系统错误,缺少必要参数”,偶现的问题但可以稳定复现。

经过排查分析发现是前端接口调用顺序问题,具体点就是调用接口3时,没有拿到需要的数据(接口3的逻辑大致是通过前端传的参数1和参数2取接口1和接口2放在缓存的数据,缓存的Key和类型有关) 从表象上看就是在调用接口3时,接口1或接口2还没有被调用,导致接口3从缓存拿不到需要的数据。

带着这样的疑问去查看前端代码,看接口的调用顺序是不是真的有问题,结果发现前端调用的顺序是没有问题的。那问题是出在哪里呢?

通过排查前端代码,发现一个问题,前端设置了一个全局变量来记录当期的业务类型(如A类型、B类型),调用接口1,2,3传递业务类型时就是传递的这个全局变量。看到这也许你就能想明白为什么说谨慎使用全局变量了,这个问题正是因为全局变量的使用不当导致的。

原因分析

我们来一起分析下到底是如何导致的吧。

上述也提到了初始化时快速切换到B类型,那么前端的这个记录当前业务类型的全局变量是何时改变其值的呢?

没错,正是在切换业务类型时记录当前业务类型A或B。当初始化默认是A类型时,接口会这样调用A类型:接口1(A)->接口2(A)->接口3(A),当切换到B类型时触发一系列接口调用,和A类型也一样,B类型:接口1(B)->接口2(B)->接口3(B)这样调用。

关键就是在切换到B类型时,可能会存在这样的问题,接口1,2正常调用,即传递的业务类型都是A,但恰好在调用接口3前,切换到了业务类型B类型,那么此时记录当前业务类型的全局变量随之变为B,那么此时原本初始化的时候的接口3拿到的业务类型就由预期的A变成了B,而在此之前接口1,2都是按A类型传递的参数,故后台存储的数据是A类型的,但此时因为全局变量的变化,接口3传递的业务类型就又A变为B,故在接口3的业务逻辑里,按业务类型B去缓存取数据时是取不到,后端校验参数时就会报错“系统错误,缺少必要参数”。

看到这,你是不是觉得这有点像java的多线程共享变量?多线程共享变量也会引发这样的问题,当一个线程正在使用某一变量时,突然被别的线程修改了,导致该线程拿到了脏数据。解决办法是,线程独享资源的操作权,操作完毕其他线程才有权限读取该资源,同一时间只有一个线程才能修改共享变量,即多个线程间相对于该资源是互斥的关系,java中多用锁来保证操作的安全性。

那在这个问题中,怎么类比呢?我们可以把选中A类型时要走的一系列接口比作A线程;把B类型要走的一系列接口比作B线程,这两个线程执行的流程、方法一样,只是需要的参数的具体值是不一样的,A、B线程各自执行三个步骤每个步骤都会取共享变量作为参数传递给后台。再把切换类型要改变当前业务类型(biz_type)这一操作记作C线程。那大致就是,A、B线程读 biz_type ,C线程修改 biz_type 。这就可以理解成三个线程共享一个变量,在页面上切换业务类型可以看做线程的轮转,所以不加以控制难免会发生错误。

问题解决

弄懂了发生问题的原因之后怎么来解决呢?其实解决起来也简单,正如标题所说[谨慎使用全局变量],问题的根源就是使用了全局共享变量,导致在A线程还没走完时C线程修改了 biz_type 的值,从而导致线程A的三个步骤拿到的 biz_type 的值不相同,进而导致后台根据类型取缓存数据时拿不到,最终报错。

所以,想要解决该问题,最关键的就是从这个全局变量着手,经查看前端代码而知:在切换类型时,根据当前选中的类型传递相应的参数,当选中时我们就能知道是哪种类型了,所以我们就能清楚的去调用接口传递相应的类型字段,而不是先对全局变量赋值,再在接口里自行去取全局变量。

修改前:

var biz_type = 'A';//定义全局变量,默认为A业务类型
//change radio
function changeRadio(){
    if(#('#bizType_A').is(':checked')){
        biz_type = 'A';//修改变量值
        api_1();
    }else{
        biz_type = 'B';//修改变量值
        api_1();
    }   
}
//function1
function api_1(){
    //get biz_type
    //send ajax with biz_typ
    if(data.success){
        api_2();
    }else{
        alert(data.msg);
    }
}
//function2
function api_2(){
    //get biz_type
    //send ajax with biz_typ
    if(data.success){
        api_3();
    }else{
        alert(data.msg);
    }
}
//function3
function api_3(){
    //get biz_type
    //send ajax with biz_type
    if(data.success){
        jump_to_success();
    }else{
        alert(data.msg);
    }
}

修改后:

//change radio
function changeRadio(){
    if(#('#bizType_A').is(':checked')){
        api_1('A');//参数传递
    }else{
        api_1('B');//参数传递
    }   
}
//function1
function api_1(biz_type){
    //send ajax with biz_typ
    if(data.success){data.
        api_2(biz_type);
    }else{
        alert(data.msg);
    }
}
//function2
function api_2(biz_type){
    //send ajax with biz_typ
    if(data.success){
        api_3(biz_type);
    }else{
        alert(data.msg);
    }
}
//function3
function api_3(biz_type){
    //send ajax with biz_type
    if(data.success){
        jump_to_success();
    }else{
        alert(data.msg);
    }
}

修改后使用参数传递的方式,这样可以保证一套流程走下来,拿到的 biz_type 值一样。

另外,可以通过控制切换的方式保证A线程没走完时不允许修改 biz_type 的值,不允许执行B线程,即当A类型下的流程没走完时切换不了类型。可以通过标志位来判定A流程是否走完,进而判定是否可以切换到B类型上。

总结

不过这个问题不大,后端做了参数的校验,但是为了提升用户体验这个问题一定是要解决的。这其实是前端开发人员一个小小的疏忽导致的,当前端在写代码时他肯定不会预见到会发生这样的问题,他肯定不会想到全局变量会导致这样的问题,更不会想到用户在页面没初始化完成时就切换类型。但这些对于一个初出茅庐的前端开发来说,情有可原,权当是积累经验了。切记能传参的尽量不要用全局变量。

出问题不可怕,在问题中成长,积累经验,才是最重要的。

本文分享自微信公众号 - 编程大道(learn_code)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android必知必会

Android 必知必会 - 根据包名判断 App 运行状态

对于没有 Service 的 App,程序一旦切换到后台,可能很快就被回收了,这里使用 ActivityManager.getRunningTasks(int ...

23320
来自专栏SpringCloud专栏

Docker下实战zabbix三部曲之三:自定义监控项

版权声明:欢迎转载,请注明出处,谢谢。 ...

7830
来自专栏Android必知必会

Android 必知必会 - DialogFragment 使用总结

Android 官方推荐使用 DialogFragment 来代替 Dialog ,可以让它具有更高的可复用性(降低耦合)和更好的便利性(很好的处理屏幕翻转的情...

25320
来自专栏呼延

监听nginx日志实现博客访问计数

但是由于我的博客项目,是基于Jekyll的,是一个静态的站点,也就是说没有普通Web项目的后端部分.

14820
来自专栏呼延

Spring Boot Mybatis Web 开发环境搭建

Spring Boot 使您能轻松地创建独立的、生产级的、基于 Spring 且能直接运行的应用程序。我们对 Spring 平台和第三方库有自己的看法,所以您从...

12650
来自专栏呼延

Jekyll监听文件变化的问题解决

Jekyll可以启动一个server服务,启动参数中有--watch(监听文件变化)和--detach(后台运行)选项,看起来这两个参数一起使用就完事了.

15810
来自专栏呼延

使用aop统一处理controller中的异常及日志

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技...

48620
来自专栏青青天空树

手把手教你学会 基于JWT的单点登录

  最近我们组要给负责的一个管理系统 A 集成另外一个系统 B,为了让用户使用更加便捷,避免多个系统重复登录,希望能够达到这样的效果——用户只需登录一次就能够在...

52950
来自专栏Web技术布道师

困扰已久的问题 cgi、fastcgi、PHP-fpm 汇总

无论是php,python编程语言,还是apache,nginx服务器对于cgi协议是个绕不开的话题。安装,部署都会经常的看到,那么它们到底是干什么的,网上的答...

16520
来自专栏编程微刊

uniapp学习笔记:一套代码,运行到7个平台(一)

前两天总结了一下小程序的一些开源的框架之后,有大佬在底下留言评论补充,uniapp没有写上,去年有小伙伴把我拉到这个群聊里面去了,当时还没有怎么了解这个框架,当...

63720

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励