Bpazy / blog

我的博客,欢迎关注和讨论
https://github.com/Bpazy/blog/issues
MIT License
41 stars 2 forks source link

搜索精度丢失:大值double的科学计数法 #285

Open Bpazy opened 1 year ago

Bpazy commented 1 year ago

背景

库存的出入库明细搜索中的值与数据库不一致,数据库时正确的数值,ElasticSearch 中因为科学计数法的问题丢失了精度。 image

调查

首先调查方向是 ES 中 subtotal 字段的类型不正确导致丢失精度,但是查看了下发现 subtotal 的类型是 scaled_float,完全可以存放该数字。 image

于是调查方向就改变为 ES 中新增记录时的入参。 出入库明细搜索的原理为:stock 工程在出入库时,会以流水级别发送 kafka 消息给 stock-search-index, index 收到消息后再将消息存入 ES 中。

于是断点在 index 收消息处,然后构造一个相同的数字的手工入库单:数量为 111111,成本为 1111.11。入库后,收到的消息如下: image

可以看见收到的 JSON 已经是错误的值了。

于是查看发消息的代码,再次打断点: image

可以看到 subtotal 已经变成科学计数法了,但是通过利用 BigDecimal 转出来的字符串还是能正常展示的。

问题原因

java 对任何超过 9999999 的值都会用科学计数法表示,这一点可以从 Double.java 中的注释看到: image

fastjson 也使用了相同的处理: image image

解决方案

那怎么避免呢?有多个方向。

方向1:从类型彻底解决问题

将代码中的类型改为 BigDecimal,影响范围可能会很大,需要判断代码是否能够支撑。实际判断下来影响范围很小。

方向2:从序列化着手

在上面的问题原因中我们知道,利用 BigDecimal 可以规避这个问题,那只要我们找一下 fastjson 是否有针对这种情况的特殊处理即可,经过查询,找到以下方法:

public static void main(String[] args) {
    SerializeConfig config = new SerializeConfig();
    config.put(Double.class, new DoubleSerializer("#.######"));
    String json = JSON.toJSONString(new Content(), config);
    System.out.println(json);
}

@Data
private static class Content {
    private BigDecimal a = new BigDecimal(12345678.1234);
}

利用 DoubleSerializer 处理即可,其原理类似于 BigDecimal,内部使用的是 DecimalFormat 来处理 double 的科学计数问题,但是这中方向会带来几个问题:

  1. 当前普遍情况是,使用 fastjson 时没有包装类,这会导致修改不统一,未来还有人可能踩坑;
  2. fastjson 有全局的 SerializeConfig,可以利用,但是要有合适的时机来修改该静态变量,极易出现静态变量乱飞的情况。 image

结论

综上 方向1 是比较好的,代码简单,影响可控,易于理解。我们只要修改 stock 工程代码即可,这样就能获得正确的 json,即使 index 仍用 double 来接,也可以正确的写入 ES 中。