JS长整型精度问题引发的一个Bug

最近在做一个IM相关应用的后端接口工作,在前后端联调的过程中出现了一个因JS浮点数精度引发的bug,记录如下。

先简单的介绍下我正在做的工作,我们在做的事情是基于公司云平台的产品探索,目前小组负责的探索方向有反馈系统、技术社区、产品点评、需求发布等,而我所在的小组是反馈系统里面衍生的在线支持。在线支持需要连接服务方和用户方,这里的服务方可以理解为淘宝卖家,不过在我们云平台上是技术能力提供方(比如FDS/HBase等),而反馈系统和在线支持,就充当起淘宝旺旺的功能,给商家和用户建立一个渠道,方便快速接入用户并解答疑问,同时还基于内部Jira系统将用户数据提交成需求单/缺陷单/建议单等,进一步提升技术迭代速度、提升用户使用体验、助力业务领跑。

介绍完背景后,让我们来看下具体出现bug的场景:

我们第一版实现的是网页群聊,是基于小米开源的MIMC来实现的。其中获取用户群聊未读消息条数的逻辑,是基于MIMC回调消息和前端在收到消息后向后端更新已读的sequence来做的。举个例子就是群聊G发送了a,b,c三条消息,用户U上次已读消息的sequence号是a的sequence号,所以此时a在群聊G有2条未读消息(消息b,c都不是用户U发的),当用户点进群聊的时候,前端取最后一条消息的sequence向后端接口更新用户的已读消息sequence。到了这里一切看起来都没有问题,但是联调过程中出现了群聊未读消息更新不成功,多次更新后依然还是会有一条未读消息的现象…

首先观察接口返回,在chrome浏览器里面F12 preview看接口返回数据,初看起来获取到的sequence返回正常,然后再去看k8s里面pod的日志,前端->后端->MIMC->MIMC回调,一切交互逻辑也是OK的。此时仔细观察了数据库里面更新回来的sequence序列号,和日志中发送给MIMC也是一致的,这个时候再去看接口中显示的群聊历史消息的sequence,此时发现了一个问题:群聊历史消息接口获取到的sequence序列号总是比收到MIMC回调的sequence小1,而在浏览器中观察接口的返回发现,Preview里面预览看到的结果,和Response里面的结果相差1!问题就在这,如下图所示:

注意看红色圈出来的部分相差1,这就是导致sequence更新不正常的元凶!

问题找到了,想到了可能是JS的精度问题,因为对JS了解的不多想着返回long型了也不是浮点数,怎么就不能精确表示了呢?看着也不像整型溢出,于是google了一下,了解到原来JS对所有的数字都使用64位浮点数表示,而浮点数虽然能表示范围很大,但是在一些大整数的时候存在不能精确表示的问题,比如上图中差1的情况,具体原因是因为在64位浮点数表示里面,只有52位是真正用来表示尾数部分的,而剩下的12位则用来表示指数和符号位,通过正规化,我们可以使用53位来表示尾数(+符号位),因此,64位浮点数能表示的尾数最大值是:2^53-1(9007199254740991),而我们接口中返回的157180098717096001远超过9007199254740991,从而JS不能精确的表示,出现了精度问题。stackoverflow上面的解释如图:

好了,到了这里终于是水落石出了,知道原因后要解决这个问题也很简单,那就是将返回字段的类型由long改为String,这样前端就能准确的获取到不会有精度问题,而且String类型也能满足我们的场景需要:群聊消息排序、更新已读sequence等。再补充一下,这里sequence使用long存储而不是String,是因为MIMC的SDK中返回的sequence就是long型的。

吃一堑,长一智。通过这个问题,我们深刻的理解了JS和前后端数据交互中可能存在的精度表达问题(准确来说是浮点数不能精确表示的问题,因为这里JS默认都使用了64位浮点数表示数字),针对这个问题NodeJs等也有自己的一个精确表示大整数数字的库来解决。作为后端开发的我们,平时多了解一下上下游的知识还是很有帮助的;再有一个就是,当时我遇到这个问题的时候,第一个感觉就是这个问题很有意思,激起了我的排查兴趣,虽然排查过程不算复杂,但是一个积极的心态对推进bug排查很有帮助的,用积极的心态去看待问题,把问题当做学习成长的机会,你将会活在当下。Coding in the moment. :-)