【问题标题】:Generate a million unique random 12 digit numbers生成一百万个唯一的随机 12 位数字
【发布时间】:2016-11-01 09:54:01
【问题描述】:

我需要为刮刮卡应用程序生成接近一百万(100 批 10000 个数字)唯一且随机的 12 位代码。此过程将重复进行,并且每次都需要生成相同数量的代码。

还需要将生成的代码输入到数据库中,以便稍后在消费者在我的网站上输入此代码时对其进行验证。我正在使用 PHP 和 Mysql 来执行此操作。这些是我正在遵循的步骤

  1. 获取有关批次数量和每批次代码的管理员输入

  2. 使用 for 循环生成代码 mt_rand(100000000000,999999999999)

  3. 每次生成数字时检查是否存在重复 在数据库中,如果不添加到结果变量中,则重新生成。

  4. 如果唯一,则将生成的数字保存在数据库中

  5. 在所需的代码数量上重复 b、c 和 d

  6. 在 csv 中向管理员输出代码

使用的代码(删除了大部分 cmets 以使其不那么冗长,因为我之前已经解释了这些步骤):

$totalLabels = $numBatch*$numLabelsPerBatch;
// file name for download
$fileName = $customerName."_scratchcodes_" . date('Ymdhs') . ".csv";
$flag = false;
$generatedCodeInfo = array();
// headers for download
header("Content-Disposition: attachment; filename=\"$fileName\"");
header("Content-Type: application/vnd.ms-excel");
$codeObject = new Codes();
//get new batch number 
$batchNumber = $codeObject->getLastBatchNumber() + 1;
$random = array();
for ($i = 0; $i < $totalLabels; $i++) {
    do{
        $random[$i] = mt_rand(100000000000,999999999999); //need to optimize this to reduce collisions given the databse will be grow
    }while(isCodeNotUnique($random[$i],$db));
    $codeObject = new Codes();
    $codeObject->UID = $random[$i];
    $codeObject->customerName = $customerName;
    $codeObject->batchNumber = $batchNumber;
    $generatedCodeInfo[$i] = $codeObject->addCode();

    //change batch number for next batch
    if($i == ($numLabelsPerBatch-1)){$batchNumber++;}


    //$generatedCodeInfo[i] = array("UID" => 10001,"OID"=>$random[$i]);
    if(!$flag) {
        // display column names as first row
        echo implode("\t", array_keys($generatedCodeInfo[$i])) . "\n";
        $flag = true;
    }
    // filter data
    array_walk($generatedCodeInfo[$i], 'filterData');
    echo implode("\t", array_values($generatedCodeInfo[$i])) . "\n";


}


function filterData(&$str)
{
    $str = preg_replace("/\t/", "\\t", $str);
    $str = preg_replace("/\r?\n/", "\\n", $str);
    if(strstr($str, '"')) $str = '"' . str_replace('"', '""', $str) . '"';
}

function isCodeNotUnique($random){
    $codeObject = new Codes();
    $codeObject->UID = $random;
    if(!empty($codeObject->getCodeByUID())){
        return true;
    }
    return false;
}

现在这需要很长时间才能执行,我认为这不是最佳选择。

  1. 如何优化才能快速生成唯一随机数?

  2. 如果用 mysql 或其他方式而不是 php 生成数字会更快吗?如果是,我该怎么做?

  3. 当 db 开始增长时,步骤 b 中的重复检查将非常耗时,那么我该如何避免呢?

  4. mysql中的行数有限制吗?

注意:数字需要在应用程序生命周期内的所有批次中唯一。

【问题讨论】:

  • 数据库中是否已有代码?如果没有,您可以在 PHP 中生成所有代码并在单个事务中将它们推送到 MySQL。在 PHP 中检查重复项可能会更快。在现代机器上,100 万个数字基本上算不了什么。 PHP 和 MySQL 都可以轻松搞定。
  • 3) 如果您的数据库被正确索引,则不会; 4)对于这个相对较小的数据量,没问题,几千亿是大的,一百万不是
  • @apokryfos 不是今天,但是一旦我生成了第一批,这将继续增长。您能否详细说明“如果不是,您可以在 PHP 中生成所有代码并在单个事务中将它们推送到 MySQL。在 PHP 中检查重复可能会更快。”
  • 我会做影子的方法。 1) 生成 100 个随机 3 位数字(批次)(范围 100 - 999)。 2) 为每个批次生成 10000 个唯一数字(范围 0 - 999999999)。不会有很多碰撞,所以它会很快。将批次 ID 与 9 位批次号结合起来,得到一个 12 位的唯一编号。
  • 这里的完整实现:​​pastebin.com/Wmh9ueZ1。它一次生成并保存一批,因此不应该存在内存问题。修改常量以获取生成的完整值并保存。在我的系统上,数据库保存非常慢(每批 30 秒),但它是“双仓鼠驱动”电脑 :)

标签: php mysql optimization random unique


【解决方案1】:

1) 根据批次数将您的数字范围划分为更小的范围。例如。如果您的范围为 0 - 1000 并且您有 10 个批次,则批次范围为 0 - 99,接下来的批次为 100 - 199,等等。当您为批次生成数字时,仅从批次范围内生成随机数。这样您就知道批次中只能有重复的数字。

不要将每个数字单独插入数据库,而是将它们存储在数组中。当您生成一个新的随机数时,然后使用 in_array() 函数检查数组,而不是数据库。批处理完成后,使用单个insert 语句插入批处理的内容:

insert into yourtable (bignumber) values (1), (2), ..., (n)

检查MySQL的max_allowed_packet设置,看是否能一次性接收完整的sql语句。

实施后备计划,以防在插入期间仍发现重复值(错误处理和数字重新生成)。

2) MySQL 在程序方面不是很好,所以我会坚持使用外部语言,例如 php。

3) 在包含随机数的字段上添加唯一索引。如果您尝试插入重复记录,MySQL 将阻止它并抛出错误。真的很快。

4) 根据使用的实际表引擎(innodb、myisam 等)、其配置和操作系统,表的大小可能存在某些限制。有关更详细的答案,请参阅 SO 上的Maximum number of records in a MySQL database table 问题(查看最受好评的答案,而不是接受的答案)。

【讨论】:

  • 我不明白您在步骤 1 中的第一段。您能举个例子吗?
  • 这个 - 根据批次数将您的数字范围划分为更小的范围。
  • 我确实在答案中为您提供了一个示例。有什么不清楚的地方?
  • Ryan 的评论帮助我理解了这个概念。但是插入到 yourtable (bignumber) 值 (1)、(2)、...、(n) 会导致 PHP 中出现“允许的 134217728 字节的内存大小已用尽(尝试分配 79 字节)”错误。知道如何避免这种情况吗?
  • 限制批量大小(批量生成的唯一数字的数量)并释放不需要的变量。
【解决方案2】:

您可以执行以下操作:

$random = getExistingCodes(); // Get what you already have (from the DB).  
$random = array_flip($random); //Make them into keys
$existingCount = count($random); //The codes you already have 

do {
    $random[mt_rand(100000000000,999999999999)] = 1;
} while ((count($random)-$existingCount) < $totalLabels);

$random = array_keys($random);

当您生成重复数字时,它只会覆盖该键而不增加计数。

要插入,您可以启动事务并根据需要执行尽可能多的插入。 MySQL 将尝试优化单个事务中的所有操作。

【讨论】:

  • 这需要我使用 $newCodes = array_diff($random,$existingRandom);提取新代码但由于内存限制而同样失败(允许的内存大小为 134217728 字节已用尽)
【解决方案3】:

这是一个生成 100 万个不重复的伪随机数的查询:

select cast(  (@n := (13*@n + 97) % 899999999981)+1e11 as char(12)) as num
from   (select @n := floor(rand() * 9e11) ) init,
       (select 1 union select 2) m01,
       (select 1 union select 2) m02,
       (select 1 union select 2) m03,
       (select 1 union select 2) m04,
       (select 1 union select 2) m05,
       (select 1 union select 2) m06,
       (select 1 union select 2) m07,
       (select 1 union select 2) m08,
       (select 1 union select 2) m09,
       (select 1 union select 2) m10,
       (select 1 union select 2) m11,
       (select 1 union select 2) m12,
       (select 1 union select 2) m13,
       (select 1 union select 2) m14,
       (select 1 union select 2) m15,
       (select 1 union select 2) m16,
       (select 1 union select 2) m17,
       (select 1 union select 2) m18,
       (select 1 union select 2) m19,
       (select 1 union select 2) m20
limit 1000000;

工作原理

它首先生成一个随机整数值 n,其中 0 。这个数字将具有生成序列的种子的功能:

@n := floor(rand() * 9e11)

通过内联记录对的多个 (20) 连接,该单条记录被乘以 220 个副本,即略多于 100 万。

然后选择开始,随着一个又一个记录的获取,@n变量的值根据这个增量公式进行修改:

@n := (13*@n + 97) % 899999999981

这个公式是linear congruential generator。这三个常数需要遵守一些规则来最大化(不重复的)周期,但是当 899999999981 是素数时它是最容易的,它就是。在这种情况下,我们有一个周期 899999999981,这意味着第一个 899999999981 生成的数字将是唯一的(我们需要的更少)。这个数字实际上是 900000000000 以下的最大素数。

作为最后一步,将 100000000000 添加到数字以确保数字始终为 12 位,因此不包括小于 100000000000 的数字。由于选择 899999999981,将有 20 个永远不会生成的数字,即介于 999999999981 和 999999999999 之间的那些。

由于这会生成 220 条记录,limit 子句将确保将其截断为恰好一百万条记录。

castchar(12) 是可选的,但可能需要在不以科学计数法在屏幕上呈现 12 位数字的情况下进行可视化。如果你要使用它来插入记录,并且目标数据类型是数字,那么你当然可以省略这个转换。

【讨论】:

  • 如果我们想要 100 个随机数,我们使用 100 的限制,如果我们想要随机数为 14 位,我们将 899999999981 更改为 90000000000000 以下的最大素数,我们可以称之为 lar。第一个选择变成 select cast( (@n := (13*@n + 97) % lar)+1e14 as char(12)) as num。美丽的逻辑
【解决方案4】:
CREATE TABLE x (v BIGINT(12) ZEROFILL NOT NULL PRIMARY KEY);

INSERT IGNORE INTO x (v) VALUES
    (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()),
    (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()),
    (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()),
    (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()),
    (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND()), (FLOOR(1e12*RAND());

执行 INSERT 1e6/15 次。

检查COUNT(*) 看看你是否有一百万。执行此操作,直到表格达到一百万行:

INSERT IGNORE INTO x (v) VALUES
    (FLOOR(1e12*RAND());

注意事项:

  • ZEROFILL 假设您希望显示具有前导零。
  • IGNORE 是因为会有一些重复。这样可以避免每次插入后进行昂贵的检查。
  • “批量插入”一次比一行快。 (一次做 100 次是最佳的,但我很懒。)
  • 潜在问题:虽然我认为 RAND() 的值模式不会重复,例如 2^16 或 2^32 值,但我不知道事实。如果你不能达到一百万,那么随机数生成器就不好;你应该切换到 PHP 的 rand,或者别的什么。
  • 当心线性结果随机数生成器。他们可能很容易被黑客入侵。 (我假设刮刮卡背后有一些“钱”。)

【讨论】:

【解决方案5】:

不要计划 mt_rand() 在小范围内是唯一的

<?php
// Does mt_rand() repeat?

TryMT(100);
TryMT(100);
TryMT(1000);
TryMT(10000);
TryMT(1e6);
TryMT(1e8);
TryMT(1e10);
TryMT(1e12);
TryMT(1e14);

function TryMT($max) {
    $h = [];
    for ($j = 0; $j<$max; $j++) {
        $v = mt_rand(1, $max);
        if (isset($h[$v])) {
            echo "Dup after $j iterations (limit=$max)<br>\n";

            return;
        }
        $h[$v] = 1;
    }
}

样本输出:

Dup after 7 iterations (limit=100)<br>
Dup after 13 iterations (limit=100)<br>
Dup after 29 iterations (limit=1000)<br>
Dup after 253 iterations (limit=10000)<br>
Dup after 245 iterations (limit=1000000)<br>
Dup after 3407 iterations (limit=100000000)<br>
Dup after 29667 iterations (limit=10000000000)<br>
Dup after 82046 iterations (limit=1000000000000)<br>
Dup after 42603 iterations (limit=1.0E+14)<br>

mt_rand() 是一个“好的”随机数,因为它确实有重复。

【讨论】:

    猜你喜欢
    • 2014-04-07
    • 1970-01-01
    • 1970-01-01
    • 2011-01-28
    • 1970-01-01
    • 1970-01-01
    • 2018-05-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多