PHP7下除0返回NAN错误

线上的一个项目,过去一周数据展示异常。排查的过程中发现PHP7和PHP5的一个有趣的差异,特记录下来。

为了尽量把问题描述清楚,单独抽象了一个极简模型:电视节目排行榜,每个电视节目有4项数据(阅读数、互动数、搜索数、播放数),假如数据已经生成好,要求根据这4项数据制作一个Top100榜单,显示节目排名和分值(0-100之间)

模拟数据如下(真实数据可能来自数据部门,通过HTTP接口获取JSON数据,或者MQ获取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$data = [
        [
            'id'    => 1,
            'name'  => '如懿传',
            'read_count'        => 11714096,
            'interactive_count' => 230,
            'search_count'      => 1078,
            'play_count'        =>  20,
        ],
        [
            'id'    => 2,
            'name'  => '甄嬛传',
            'read_count'        => 21714096,
            'interactive_count' => 410,
            'search_count'      => 900,
            'play_count'        =>  9,
        ],
        [
            'id'    => 3,
            'name'  => '芈月传',
            'read_count'        => 8018618,
            'interactive_count' => 333,
            'search_count'      => 1700,
            'play_count'        =>  2,
        ],
    ];

算法:
0、对每一个节目每个维度的数据进行归一化处理A1, B1, C1, D1。
1、定义每个维度的权重w1, w2, w3, w4(w1 + w2 + w3 + w4 = 1)。
2、计算每个节目的分值score。每个维度归一化后的数据 * 相应权重求和(score = A1 * w1 + B1 * w2 + C1 * w3 + D1 * w4)。
3、按分值排序。
4、计算排名。

计算归一值的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
  * 计算归一值
  * @param $ini int 当前值
  * @param $max int 当前维度最大值
  * @param $min int 当前维度最小值
  *
  * @return float 分值
  */
public static function normal($ini, $max, $min) {
     $max = log(1 + $max);
     $min = log(1 + $min);
     return (log(1 + $ini) - $min) / ($max- $min);
 }

计算分值

1
2
3
4
5
6
7
8
9
10
11
12
13
public static function get_score($data, $weight) {
        $data['read']           = self::normal($data['read_count'], $data['read_max'], $data['read_min']);
        $data['interactive']            = self::normal($data['interactive_count'], $data['interactive_max'], $data['interactive_min']);
        $data['search']         = self::normal($data['search_count'], $data['search_max'], $data['search_min']);
        $data['play']           = self::normal($data['play_count'], $data['play_max'], $data['play_min']);
 
        $add = $data['read'] * $weight['w1'] + $data['interactive'] * $weight['w2'] +
                $data['search'] * $weight['w3'] + $data['play'] * $weight['w4'];
 
        $score = log($add + 1, 2);
 
        return $score;
    }

以上是部分实现,在归一化的时候,max和min一般不会都为0, 确实在线上跑了2年也没出问题,但是前不久,数据依赖方做了调整play这一项数据全部为0,导致方法normal()中除数为0,返回NAN,get_score()也返回NAN。根本原因还是没有检查除数为0的情况。

事后验证对比发现,
PHP5下除数为0返回false, PHP7下除数为0返回NAN。
PHP中1 + false = 1,1 + NAN = NAN;
PHP中json_encode($data),$data中值包含NAN将返回false。
PHP中的NAN入到MySQL中显示为0。

测试代码:线上查看点击这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
echo date('Y-m-d H:i:s') . PHP_EOL;
echo "PHP版本: " . phpversion() . PHP_EOL;
 
$data = 0 / 0;
$res = 100 + $data;
 
$count = [
    'id' => 1,
    'read_count' => 100,
    'score' => NAN,
    ];
var_dump($data, $res, $count, json_encode($count), 2333);die;
 
// PHP5返回结果begin
2018-09-24 09:34:18
PHP版本: 5.6.9-0+deb8u1
bool(false)
int(100)
array(3) {
  ["id"]=>
  int(1)
  ["read_count"]=>
  int(100)
  ["score"]=>
  float(NAN)
}
bool(false)
int(2333)
 
 
PHP Warning:  Division by zero in /usercode/file.php on line 5
 
// PHP7下返回结果
2018-09-24 09:33:32
PHP版本: 7.0.0-dev
 
Warning: Division by zero in /usercode/file.php on line 5
float(NAN)
float(NAN)
array(3) {
  ["id"]=>
  int(1)
  ["read_count"]=>
  int(100)
  ["score"]=>
  float(NAN)
}
bool(false)
int(2333)

总结:
0、浏览代码的时候一定要看完整,不要漏了某一行,导致逻辑看不懂。
1、解决问题的正确方式,复现现象,在有问题的代码中加日志。加日志的时候,缩小日志的记录,方便查看定位问题。切记不要猜测,有些问题的原因根本是猜不到的。【谁会想到PHP7下0 /0会返回NAN,谁能想到NAN + 1 = NAN,谁能想到json_encode()的数组中带NAN返回异常,谁能想到NAN入到MySQL中为0 】
2、时刻提醒自己线上环境和验证环境是否完全一直。最好开发环境和生产环境一直,起码PHP版本要一致。
3、长期运行稳定的代码突然出问题,一定要了解下其他方近期做了什么调整,依赖方上下线了什么,自己上线了什么,运营调整了什么运维调整了什么。

发表评论

电子邮件地址不会被公开。 必填项已用*标注