微信页面注册时重复请求的问题
以下转载于同事空间日志,做个记录:
最近在微信端的开发当中发现微信浏览器会重复请求后台,搞的我是寝食难安,业务逻辑就是一用户注册逻辑,本地测试没问题,放生产上有时就会报“用户已存在”异常,开始以为是代码逻里有重复插入的逻辑,在我来来回回反反复复的看了n遍依然没有发现有重复插入的逻辑时,我又把注意力移到了jquery的ajax上,以为是jquery里ajax重复提交的,于是我改变ajax提交为普通的href连接,结果错误依然。这时我感觉很可能是尼玛微信的套路,最后用了一很勉强的办法暂时先处理了一下:
一旦发现错误是类似"ORA-00001"的这种违反唯一约束的错误时,一律给予用户注册成功的响应。
然后我在网上找了一下资料,以下我是从网上找了一篇比较好的文章,拿出来和大家分享一下。
背景
这是个大坑,耗费了我极多的时间。
事情呢,是这样的。最近几天做了一个微信里的潜入页,用于注册账户的。注册很简单,输入手机号-验证短信验证码-填一点资料-注册成功。
作为一个单页面操作,所有请求都是通过AJAX和服务器交互的,这思路很常规。唯一的特点是,最后一步超长。
超长的原因是:创建账户需要创建几百张表,还有无数初始化操作,所以乐观估计需要至少八秒钟才会成功。
也许你会问为什么会这么慢,要创建这么多表呢?原谅我不想说,因为与本文无关。
然则这个创建其实是有步骤的,第一步就是把用户的邮箱给占位:创建为最基本的信息,不允许重复创建。
OK,背景说完。
直到上线
开发,测试,包括内测都是很完美的,没有任何问题。
但上线后,突然有测试提出了这么一个问题:微信里注册的时候,任何一个邮箱都会提示邮箱已使用。
面对这样一个错误且不管怎么换邮箱都同样的错误,所有人的表情都是懵逼的。
内心OS:这特么什么鬼。
后端(Java,没错,语法和性能巨烂的Java):这是个很低级的BUG,很明显是服务器首次返回错误信息后乃们直接缓存了记录信息,所以后面其实都没验证。
我还没去反驳,测试直接打脸。
测试:我第一次输入就这样。
Java:……
Java同学陷入了沉思。反正我带着『不是自己问题』的乐观心态,坐在旁边看他们三脸懵逼。后来Java同学很不乐意地回去看了下数据库,发现邮箱确实存在了。
Java同学一拍大腿,说:哎呀你们这群测试都是猪呀,这明显存在嘛!
测试的同学一脸懵逼,极不情愿地又用了一个小号邮箱重新注册,一样的错误。Java同学一查数据库,“行不行啊你们,这邮箱还是有啊”。
测试同学一脸茫然,说我准备重新申请个QQ,你丫别吵吵……你看看XXX这个邮箱存不存在。
Java:这个邮箱不存在。
测试同学低头鼓捣了一下,说,“一样的错误”。Java同学低头查了一下,哎哟卧槽,怎么数据库有记录了,不是返回已存在了么。
然后所有人都沉默了一下,最后他们把头一起转向我,表情是这样的。
他们:一定是你重复发请求却只拿最后一次请求当结果了。
我直接甩锅:别看我,关我毛事,所有浏览器都测试过,按钮在发请求的时候都禁用了不可能重复点击,发请求状态都有标记不可能重复发,怎么可能发两次,你们别闹了,证据呢就乱说话。
Java同学回去给代码加上Log输出,然后一边测一边看输出。
然后他们看到了对我很不利的结果:“木鱼你看,你就是发出了两次请求嘛!”
我看了看记录,特么的果然前后差了两秒钟,有两次开始执行记录的请求……
我……
漫长地找原因
我极不情愿地回来找前端的问题。
然而我不管怎么看代码,怎么调试,怎么用自己的微信测试,不管是安卓(小米)还是iPhone5S,都测不出半点问题。从程序逻辑上看也根本没有发两次请求的机会。
好郁闷……用了MVVM框架,难道这框架有问题??
然后我就开始怀疑是服务器那边的问题。Java的后端是跑在Tomcat上的,前面还有个Nginx做反代。于是我到运维那边找Ngxin的访问日志看了一下。
查看了前后关联的请求,确实有重复的,但是客户端IP都不一样,时间也不一样,而同一个IP的请求前后都是一个序列不存在重复。
所以根据Nginx的日志,很明显客户端没有重复请求啊,要是重复请求了,应该看到同一个IP的请求会重复出现才对。那么Java那边的重复记录怎么回事,难道Java这边自己调用了两次??
看着Java同学那么天真的脸,我觉得把这个锅就这样甩给他们是莫大的伤害。
于是我本着“有则改进无则加勉”的出发点,决定改前端代码。
第一次尝试,在进入ajax方法之前,加一个boolean变量标记,进入时加标记,完成后清除标记,进入之前判断是否已标记,如果已标记则直接退出。
完全没改进,依然同样的错误。
第二次尝试,将Ajax方法改成同步的,我直接阻止你浏览器操作,不能重复操作总不会因为DOM事件重复发了吧?
完全没改进,依然同样的错误。
第三次尝试,挂载全局的Ajax钩子,在ajax完成后打印返回结果到dom里。
打印信息没重复,表明只收到了一个结果,那就是邮箱已存在。
第四次尝试,直接上jQuery的AjaxFilter,将并行的重复ajax请求,直接截断。
完全没结果。
第五次尝试,在Ajax之前,设置了一个alert弹窗警告。
神奇的,重复请求没了……
这特么都是什么跟什么啊。
此时,已经从晚上八点折腾到了十点多。
为了尽快弄明白问题出在哪里,决定抓包测试。拿出自己的手机,用微信访问页面注册,完全没有任何问题。
难道这和手机有关系?那出现这问题的手机也不是一部啊?拿了一部之前一直出问题的手机过来,连wifi设代理用Fiddler抓包。
完全正常,没有任何问题,注册流程很正常。
但是取消代理,就又只会报邮箱已存在的错误。
what the fuck……
此时,已经是十一点多了。
这时候,他们提出了一个很有建设性(才怪)的建议,就说是不是因为那个alert导致请求延迟了几秒才正常的,或者这是jQuery的问题?
我很不情愿(并觉得他们纯粹是扯淡)地加了一个setTimeout
测试。
涛声依旧啊。
到底咋回事?
根据Java的输出,是有返回创建成功的消息的。
然后我将所有的Ajax结果全部显示到DOM里,发现只有错误信息,却没有那个成功的信息。换句话说,如果请求确实重复发了,那么唯一能解释的是,js运行出错导致对应的消息没处理。
然而新版本的安卓版微信自带浏览器内核,不是系统的webview,所以要调试只能用微信自己的工具。不过好歹可以测试。
连上调试器,打断点,发现ajax函数只调用一次,没问题,但是唯一收到的消息就是返回了错误,却没有正常的结果。所以不是出错,而是确实就没有返回那个结果。
看到这样的结果,所有人暴躁了,这特么到底什么事啊。
本来想在微信里抓包,然而微信调试工具里要抓包同样需要设代理,从之前测试的结果看,设代理就无此问题,又一次被卡住了。
此刻,我的内心是崩溃的,难道真的只能洗洗睡了吗?
啊,看到了曙光
此时,已经凌晨十二点多。为了尽快找到问题,后端的同学开始直接连上服务器实时输出Nginx的访问日志。
神奇的,点击一次注册,滚动出了两条日志……(原谅我没截图)……我一眼看过去,哎卧槽这不是我之前看到的那俩IP么,咋这时候还在???
看到的两条日志,除了客户端IP不一样之外,其它信息一模一样,包括地址、方法和UserAgent。客户端IP分别是123.151.42.57
和211.102.210.254
。211.102.210.254
这个没啥问题,是这边的出口IP,那前面一个 123.151.42.57 是什么鬼?在ip138上查了一下,这是个天津的电信IP。然后这个IP的请求是先发出来的,比后面的请求提前了两秒钟,然而响应状态码是499
,后面是200
。
499状态码是什么错误呢?搜索了一下Nginx的错误码,指出这个(非标)错误是指客户端关闭了连接。
……………………客户端关闭了连接????
而邮箱已存在的错误,正是后面的那一条200请求返回的。
PHP的同学一拍大腿,卧槽这不是说超时了么,你丫去把请求的超时设长一点。
我不想跟他说话并想甩他一脸翔。
我之前为了避免此问题已经将超时设成5分钟了。
不过此时,我开始关注那俩IP了……
猜测
事已至此,我唯一的猜测就是,微信的浏览器里发请求,并不是一定直接向服务器发送请求的,而是通过了某些特定的中转服务器进行转发。至于转发的原因,可能是为了重拍版(将PC的网页压缩重拍版以节约手机流量或适应大小)或加速(压缩)抑或安全检测(中转的时候拦截已知恶意网址)。往前面翻了一下,前面的几步流程都是123.151.42.57
这个IP转发的,而最后一个注册请求发出来了却在2秒后又由211.102.210.254
直接的连接发起……我只能解释为这个中转服务器的超时时间是两秒钟,为了防止服务器网络问题导致用户过长等待,默认如果请求2秒钟没返回则放弃中转改为直接发起。
如果事实真如此,那么就能解释为什么前后会跟着两个请求,而前一个请求却在2秒钟后出499错误的情况了。
那么为什么用代理的时候没有此问题?很容易解释,如果系统已经设置为通过代理服务器访问了,那么软件会直接假定无法直接联网或无法简单可靠地通过反代访问,所以会放弃中转。
从前后间隔2秒的时间判断,大概超时时间就是2秒左右。那么可以做出假设,假定请求2秒钟内就会返回,则不会引发第二次请求。
Java同学改了一下接口,先不做具体操作直接返回成功,测试了一下,果然就不会有此问题顺利走通流程。
这个坑也是……活活把人坑到了凌晨1点。
后来作为改进的创建方案,Java端在确定可以创建后,直接返回成功,然后开后台任务异步创建。
没有去校验创建是否成功,因为他们觉得失败概率很低,就算失败了其实也没有啥可重试的方法,还是先不检查算了。
到此为止,问题顺利解决。
现在问题解决了,但真正的问题还没解决:那就是我需要确定自己的结论。
根据上面的判断,有这样的结论:
1、微信里直接发出ajax请求的话,其实是通过一些特定的服务器中转的
2、这个中转不是全部的,和手机以及系统有关系(因为我的安卓手机微信就没有这现象,而有此情况的手机也不是某个特定的品牌,iPhone也没此问题)
3、这个中转的超时时间很短,一旦超时,会迅速回滚为非中转模式请求
至于这个中转到底什么情况下有,什么情况下没有,这个没有找到规律,也未知是微信中内嵌的浏览器内核行为还是系统行为。
如果说非要做一点实质性的总结,那就是,如果假定你的请求是关键请求且难以重试还跑在微信中的,最好保证你的接口在两秒钟内返回,
否则可能会有很诡异的难以复现的问题,如果时间过长的最好做成异步的。
结案陈词,就是这种中转机制设计真的很糙,也不知道是哪里引入的。