PHP7下除0返回NAN错误

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

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

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

$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、计算排名。

计算归一值的方法

   /**
	 * 计算归一值
	 * @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);
    }

计算分值

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。

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

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、长期运行稳定的代码突然出问题,一定要了解下其他方近期做了什么调整,依赖方上下线了什么,自己上线了什么,运营调整了什么运维调整了什么。

发表评论

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