事情原由
今天测试忽然在群里发了一个看似非常简单的线上问题,具体是:在后台通过订单编号(orderId)修改订单信息时,修改不成功 ,修改前后的订单数据完全没有发生变化。第一眼看到这个问题的时候,我心想后台实现逻辑并不就是一个updateById
更新订单表的操作(简化了其他业务逻辑)吗?难道订单编号(orderId)在代码里给属性赋值赋错了,心想这么低级的错误“同事”应该不会犯吧,于是我就打开ide先去看了看对应方法的处理逻辑,整体更新操作 属性之间的赋值没有问题,难道又是一个”灵异事件“?说罢 我便想着在测试环境能不能复现一下这个bug,功能上线前功能肯定是测试通过的,于是我在测试环境点啊点,在页面上模拟了几十次更新操作也没有发现问题。
此时我灵机一动,此次的这个问题不会和数据类型精度有什么关系吧,印象最深刻的是System.out.println(1.0F - 0.9F);
实际输出不是 0.1
,难道订单号用的数据类型也存在精度丢失的问题吗?
然后我便让测试把那条有问题的订单号发给我,终于功夫不负有心人,通过相同的数据完美的复现了bug(解决了一半)。
问题复现过程
为了简化繁杂的业务流程,这里就不在数据库建表了。只模拟问题的重点
订单实体类:
@Data
public class Order {
private Long id;
private Long orderId;
private String creteName;
}
@RequestMapping("/order")
@RestController
public class OrderController {
@GetMapping(value = "/get")
public Order get(){
Order order = new Order();
order.setId(1L);
order.setOrderId(362909601374617692L);
order.setCreteName("张三");
return order;
}
}
用接口测试工具测试:
这么乍一看,返回的编号没问题,和实际代码里获取到的单号一致。此时就考虑程序员的综合开发能力了,为了模拟更加贴切真实环境,通过前端去请求获取订单信息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
</body>
</html>
<script>
$(function(){
$.ajax({
url: "http://127.0.0.1:8080/order/get",
success: function(data){
$("body").text(JSON.stringify(data));
}
})
})
</script>
这时问题就完美的复现了,代码里返回的orderId是362909601374617692
,但前端通过请求获取到的却是:362909601374617660
,orderId不一致。看到这里大概就明白了,问题的原因大概是:前端的数据类型(存在精度问题)或者是http协议造成的。然后我就去查阅相关资料,最后确定原因是 :Java服务端如果直接返回Long整形数据给前端,JS会自动转换为Number类型,JS中Number 类型有些数值会有精度损失。具体原因放在最后说明,先说解决办法:既然Number类型有精度损失的问题,那我返回的时候换一个数据类型不就避免了这个问题。
解决方法
@RequestMapping("/order")
@RestController
public class OrderController {
@GetMapping(value = "/get")
public Order get(){
Order order = new Order();
order.setId(1L);
order.setOrderId("362909601374617692");
order.setCreteName("张三");
return order;
}
}
由于JS是弱类型语言,前端拿到orderId也没进行任何计算操作,所以只修改后台数据类型就可以,前端不需要修改任何代码。
原因
Java 服务端如果直接返回 Long 整型数据给前端,JS 会自动转换为 Number 类型(注:此类型为双精度浮点数,表示原理与取值范围等同于 Java 中的 Double)。Long 类型能表示的最大值是 2 的 63 次方-1,在取值范围之内,超过 2 的 53 次方 (9007199254740992)的数值转化为 JS 的 Number 时,有些数值会有精度损失。
扩展说明:在 Long 取值范围内,任何 2 的指数次整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,双精度浮点数的尾数位只有 52 位
2 的 63 次方-1 等于
9223372036854775807
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number
总结
本次问题主要是后端返回的订单编号是Long类型,在特定数值下会造成和前端拿到的orderId不一致,通过orderId再去更新时导致页面上显示的数据没有发生变化,有可能拿着不对的orderId更新到了其他不相关的数据。修改后采用"String"类型传递 orderId可以避免这个问题。实际开发中操作订单状态应该是通过PRIMARY KEY
来操作订单表,PRIMARY KEY
可以是自增id 雪花id uuid等分布式唯一id,orderId是单独的一列 非主键存储,尽量避免通过orderId操作订单数据。
至于这个问题有没有发生过,我也忘了,不过它确实是存在的。