PHP:CSI - 随机增量插入

不久前,我正在对网站进行一些更改,并遇到了一段代码,让我茫然地盯着我的屏幕。我正在开发的网站是一个自定义构建网站,由我当时合作的公司的另一位开发人员创建。我以前从未在这个网站上做过 PHP:CSI,但记得当时我对我发现的东西感到非常惊讶,所以我做了笔记以备将来参考。我最近在思考如何处理代码的分析。

我发现的代码是在一个将项目插入数据库表的方法中。出于某种原因,我无法理解开发人员选择不使用自动增量 ID,而是开发了一种方法,该方法基本上随机决定项目的 ID 号。

我无法在此处粘贴整个代码块,但我可以包含让我感到困惑的代码区域。

do {
  $ref = mt_rand(1, pow(2, 31)-1); // 生成参考编号
  $result = $mysqli->query("SELECT id FROM tblrefs WHERE ref = {$ref}");
} while($result->num_rows == 0);

如果你有足够的经验,你可能会在这里意识到这个问题。我认为逐步完成这里发生的一切可能是个好主意。

  • 首先我们进入一个 do-while 循环。这将继续运行,而 while的条件 为真。

  • 接下来我们生成一个参考号并将其存储在变量 $ref 中。这是一个随机数(由mt_rand()函数生成),介于 1 和 32 位整数的最大数之间(通过将 2 的 31 次方加减 1 来计算)。

  • 下一行是一个 SQL 查询,用于查明我们生成的随机数是否已经存在于我们感兴趣的数据库表中。

  • 然后查看 while 条件。如果从 SQL 查询返回的行数不是 0,那么我们已经在数据库中使用了该数字,因此我们需要再次循环以重新生成该数字,依此类推。如果它是 0,那么这个数字是新的,我们可以退出 do-while 循环并继续将项目插入到数据库中。

这有几个问题。

  • 要将任何内容插入表中,我们首先需要先查询数据库以查看是否有匹配的 ID。

  • 出于某种原因,我们正在使用数学计算最大整数的值,而不是内置的 PHP 常量 PHP_MAX_INT。通过将 2 乘以 31 的幂来即时计算此值实际上是一项非常昂贵的操作。为什么开发人员决定不在这里使用 PHP_MAX_INT 超出了我的理解,但这不是这里的主要问题。

  • 当我们生成一个随机数时,我们创建一个已经存在的数字的机会越来越大。这意味着数据库中的项目越多,我们就越有可能在数据库中找到现有项目,并且必须再次重新生成我们的随机数。

  • 由于这是一个繁忙的网站,有许多编辑器在编写内容,因此该代码块可能会生成两个相同的 ID,并尝试同时将它们插入到数据库中。 

作为一个快速测试,我决定看看这个方法需要多长时间才能从随机创建的值中找到重复的数字。我编写了一个快速脚本来使用与上述相同的方法生成随机数,直到找到重复项,此时我们计算创建的值的数量并重新开始。

for ($outer = 0; $outer <= 100000; ++$outer) {
  // 重置 refs 数组。
  $refs = array();
 
  do {
    // 生成随机数。
    $ref = mt_rand(1, pow(2, 31)-1);
 
    // 是不是已经生成了。
    if (in_array($ref, $refs)) {
      // 编号已存在,记录冲突并打破循环。
 
      // 计算生成的随机数的数量。
      $count = count($refs);
 
      // 将数据记录到文件中。
      file_put_contents('counts.txt', $count . "\n", FILE_APPEND);
 
      // 打破 while 循环。
      break;
    }
 
    // 循环未中断,将数字添加到列表中并继续。
    $refs[] = $ref;
 
  } while(1==1);
}

运行此脚本(需要一段时间才能完成)后,我对其进行了一些快速分析。这是结果。

Min: 824
Max: 200448
Mean: 58774.052132701
Standard Deviation: 8117.1541955524

所以事实证明,我们只需要创建几千个项目就可以开始对数字进行冲突了。该脚本仅查看第一次冲突,但很明显,这种情况持续的时间越长,我们很可能会在数据库中生成重复的 ID。

更好的方法

虽然这种方法有效,但更好的解决方案是简单地使用 MySQL 中存在的现有自动增量系统。我还是想不通为什么原来的开发者没有使用这个系统。

即使他们不使用自动增量,他们也可以使用哈希表算法来生成项目的 ID。这可以通过使用我们即将插入的信息的 SHA 生成哈希来完成。万一发生冲突,数据库会返回错误,此时我们需要找出新的哈希值。即使在这种情况下,它仍然比将 ID 随机分配给一个值并首先查看它是否存在更高效。

一旦代码沿着这条路线走下去,问题是如果没有大量的拆解,就很难切换到另一种方法。当我注意到这段代码时,我与我的经理将其标记为一项昂贵且具有潜在危险的操作,我们应该花一些时间修复它。此后不久我离开了公司,但我确实看到该站点正在迁移到不同的平台,因此我可以肯定地说此代码不再用于生产。