繁体   English   中英

使用 MySQL 生成随机且唯一的 8 个字符的字符串

[英]Generating a random & unique 8 character string using MySQL

我正在开发一款在某些时候涉及车辆的游戏。 我有一个名为“vehicles”的 MySQL 表,其中包含有关车辆的数据,包括存储车辆牌照的列“plate”。

现在到了我遇到问题的部分。 在创建新车之前,我需要找到一个未使用的车牌 - 它应该是一个字母数字 8 字符的随机字符串。 我是如何做到这一点的,是在 Lua 中使用 while 循环(我正在编程的语言)来生成字符串并查询数据库以查看它是否被使用。 然而,随着车辆数量的增加,我预计这会变得更加低效,就像现在一样。 因此,我决定尝试使用 MySQL 查询来解决这个问题。

我需要的查询应该只生成一个 8 字符的字母数字字符串,该字符串不在表中。 我再次想到了 generate&check 循环方法,但我并没有将这个问题限制在这个问题上,以防万一有更有效的方法。 我已经能够通过定义一个包含所有允许字符的字符串并随机对其进行子串化来生成字符串,仅此而已。

任何帮助表示赞赏。

我不会担心碰撞的可能性。 只需生成一个随机字符串并检查它是否存在。 如果是这样,请再试一次,除非您已经分配了大量的盘子,否则您不需要再做几次。

在纯 (My)SQL 中生成 8 个字符长的伪随机字符串的另一种解决方案:

SELECT LEFT(UUID(), 8);

您可以尝试以下(伪代码):

DO 
    SELECT LEFT(UUID(), 8) INTO @plate;
    INSERT INTO plates (@plate);
WHILE there_is_a_unique_constraint_violation
-- @plate is your newly assigned plate number

由于这篇文章受到了意想不到的关注,让我强调一下 ADTC 的评论:上面的代码非常愚蠢,并且产生了连续的数字。

对于稍微不那么愚蠢的随机性,请尝试这样的事情:

SELECT LEFT(MD5(RAND()), 8)

对于真正的(加密安全)随机性,请使用RANDOM_BYTES()而不是RAND() (但我会考虑将此逻辑上移到应用程序层)。

这个问题由两个非常不同的子问题组成:

  • 字符串必须是看似随机的
  • 字符串必须是唯一的

虽然随机性很容易实现,但没有重试循环的唯一性则不然。 这使我们首先专注于独特性。 使用AUTO_INCREMENT可以轻松实现非随机唯一性。 因此,使用保持唯一性的伪随机变换就可以了:

  • @paul 建议使用哈希
  • AES-encrypt 也适合
  • 但是有一个很好的: RAND(N)本身!

由同一种子创建的随机数序列保证是

  • 可重现的
  • 前 8 次迭代不同
  • 如果种子是INT32

所以我们使用@AndreyVolk 或@GordonLinoff 的方法,但使用种子RAND

例如 Assumin id是一个AUTO_INCREMENT列:

INSERT INTO vehicles VALUES (blah); -- leaving out the number plate
SELECT @lid:=LAST_INSERT_ID();
UPDATE vehicles SET numberplate=concat(
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@lid)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed)*36+1, 1)
)
WHERE id=@lid;

如何计算顺序整数的 MD5(或其他)哈希,然后取前 8 个字符。

IE

MD5(1) = c4ca4238a0b923820dcc509a6f75849b => c4ca4238
MD5(2) = c81e728d9d4c2f636f067f89cc14862c => c81e728d
MD5(3) = eccbc87e4b5ce2fe28308fd9f2a7baf3 => eccbc87e

等等。

警告:我不知道在碰撞之前可以分配多少(但这将是一个已知且恒定的值)。

编辑:这现在是一个旧答案,但随着时间的推移,我再次看到它,所以,从观察...

所有数字的机会 = 2.35%

所有字母的机会 = 0.05%

MD5(82945) = "7b763dcb..." 时的第一次碰撞(结果与 MD5(25302) 相同)

创建一个随机字符串

这是一个 MySQL 函数,用于创建给定长度的随机字符串。

DELIMITER $$

CREATE DEFINER=`root`@`%` FUNCTION `RandString`(length SMALLINT(3)) RETURNS varchar(100) CHARSET utf8
begin
    SET @returnStr = '';
    SET @allowedChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    SET @i = 0;

    WHILE (@i < length) DO
        SET @returnStr = CONCAT(@returnStr, substring(@allowedChars, FLOOR(RAND() * LENGTH(@allowedChars) + 1), 1));
        SET @i = @i + 1;
    END WHILE;

    RETURN @returnStr;
END

使用SELECT RANDSTRING(8)返回一个 8 个字符的字符串。

您可以自定义@allowedChars

不能保证唯一性 - 正如您将在其他解决方案的评论中看到的那样,这是不可能的。 相反,您需要生成一个字符串,检查它是否已被使用,如果是,请重试。


检查随机字符串是否已被使用

如果我们想将碰撞检查代码排除在应用程序之外,我们可以创建一个触发器:

DELIMITER $$

CREATE TRIGGER Vehicle_beforeInsert
  BEFORE INSERT ON `Vehicle`
  FOR EACH ROW
  BEGIN
    SET @vehicleId = 1;
    WHILE (@vehicleId IS NOT NULL) DO 
      SET NEW.plate = RANDSTRING(8);
      SET @vehicleId = (SELECT id FROM `Vehicle` WHERE `plate` = NEW.plate);
    END WHILE;
  END;$$
DELIMITER ;

这是一种使用字母数字作为有效字符的方法:

select concat(substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1)
             ) as LicensePlaceNumber;

请注意,不保证唯一性。 您必须单独检查。

这是另一种生成随机字符串的方法:

SELECT SUBSTRING(MD5(RAND()) FROM 1 FOR 8) AS myrandomstring

你可以使用 MySQL 的rand()char()函数:

select concat( 
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97)
) as name;

您可以使用以下命令生成随机字母数字字符串:

lpad(conv(floor(rand()*pow(36,8)), 10, 36), 8, 0);

您可以在BEFORE INSERT触发器中使用它并在 while 循环中检查重复项:

CREATE TABLE `vehicles` (
    `plate` CHAR(8) NULL DEFAULT NULL,
    `data` VARCHAR(50) NOT NULL,
    UNIQUE INDEX `plate` (`plate`)
);

DELIMITER //
CREATE TRIGGER `vehicles_before_insert` BEFORE INSERT ON `vehicles`
FOR EACH ROW BEGIN

    declare str_len int default 8;
    declare ready int default 0;
    declare rnd_str text;
    while not ready do
        set rnd_str := lpad(conv(floor(rand()*pow(36,str_len)), 10, 36), str_len, 0);
        if not exists (select * from vehicles where plate = rnd_str) then
            set new.plate = rnd_str;
            set ready := 1;
        end if;
    end while;

END//
DELIMITER ;

现在只需插入您的数据

insert into vehicles(col1, col2) values ('value1', 'value2');

触发器将为plate列生成一个值。

( sqlfiddle 演示)

如果该列允许 NULL,则以这种方式工作。 如果您希望它为 NOT NULL,则需要定义一个默认值

`plate` CHAR(8) NOT NULL DEFAULT 'default',

如果大写字母数字不是您想要的,您还可以在触发器中使用任何其他随机字符串生成算法。 但是触发器会处理唯一性。

对于由 8 个随机数和大小写字母组成的字符串,这是我的解决方案:

LPAD(LEFT(REPLACE(REPLACE(REPLACE(TO_BASE64(UNHEX(MD5(RAND()))), "/", ""), "+", ""), "=", ""), 8), 8, 0)

由内而外解释:

  1. RAND生成一个介于 0 和 1 之间的随机数
  2. MD5计算(1)、32个字符从af到0-9的MD5和
  3. UNHEX将 (2) 转换为 16 个字节,其值从 00 到 FF
  4. TO_BASE64将 (3) 编码为 base64,从 az 和 AZ 和 0-9 开始的 22 个字符加上“/”和“+”,后跟两个“=”
  5. 三个REPLACE从 (4) 中删除“/”、“+”和“=”字符
  6. LEFT取 (5) 中的前 8 个字符,如果您在随机字符串中需要更多或更少的字符,请将 8 更改为其他字符
  7. 如果长度小于 8 个字符, LPAD在 (6) 的开头插入零; 再次,如果需要,将 8 更改为其他内容

要生成随机字符串,您可以使用:

SUBSTRING(MD5(RAND()) FROM 1 FOR 8)

你收到这样的东西:

353E50CC

我使用来自另一列的数据来生成“散列”或唯一字符串

UPDATE table_name SET column_name = Right( MD5(another_column_with_data), 8 )

字母表中的 8 个字母 - 全部大写:

UPDATE `tablename` SET `tablename`.`randomstring`= concat(CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25)))CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))));

如果您没有 id 或种子,就像它在插入中的值列表一样:

REPLACE(RAND(), '.', '')

要创建一个随机的10 位字母数字,不包括相似字符 01oOlI:

LPAD(LEFT(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(TO_BASE64(UNHEX(MD5(RAND()))), "/", ""), "+", ""), "=", ""), "O", ""), "l", ""), "I", ""), "1", ""), "0", ""), "o", ""), 10), 10, 0)

这正是我创建优惠券代码所需要的。 在将其输入凭证代码表单时,混淆字符被删除以减少错误。

希望这对某人有所帮助,基于Jan Uhlig 的精彩回答

请参阅 Jan 的答案以了解此代码的工作原理。

获取包含大小写字母和数字的随机 10 个字符串的简单有效的解决方案:

select substring(base64_encode(md5(rand())) from 1+rand()*4 for 10);

考虑到您需要的字符总数,您生成两个完全相同的车牌的机会非常小。 因此,您可能可以在 LUA 中生成数字。

你有 36^8 个不同的唯一车牌(2,821,109,907,456,很多),即使你已经有一百万个车牌,你也有很小的机会生成你已经拥有的车牌,大约 0.000035%

当然,这完全取决于您最终将创建多少个车牌。

如果您对“随机”但完全可预测的车牌没问题,您可以使用线性反馈移位寄存器来选择下一个车牌号码 - 它保证在重复之前通过每个号码。 但是,如果没有一些复杂的数学运算,您将无法遍历每个 8 个字符的字母数字字符串(您将在 36^8 (78%) 个可能的盘子中得到 2^41 个)。 为了更好地填满你的空间,你可以从盘子中排除一个字母(可能是 O),给你 97%。

此函数根据您的输入长度和允许的字符生成一个随机字符串,如下所示:

SELECT str_rand(8, '23456789abcdefghijkmnpqrstuvwxyz');

功能代码:

DROP FUNCTION IF EXISTS str_rand;

DELIMITER //

CREATE FUNCTION str_rand(
    u_count INT UNSIGNED,
    v_chars TEXT
)
RETURNS TEXT
NOT DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
COMMENT ''
BEGIN
    DECLARE v_retval TEXT DEFAULT '';
    DECLARE u_pos    INT UNSIGNED;
    DECLARE u        INT UNSIGNED;

    SET u = LENGTH(v_chars);
    WHILE u_count > 0
    DO
      SET u_pos = 1 + FLOOR(RAND() * u);
      SET v_retval = CONCAT(v_retval, MID(v_chars, u_pos, 1));
      SET u_count = u_count - 1;
    END WHILE;

    RETURN v_retval;
END;
//
DELIMITER ;

此代码基于“Ross Smith II”发送的随机字符串函数

生成 8 个字符的密钥

lpad(conv(floor(rand()*pow(36,6)), 10, 36), 8, 0); 

如何为我的 MySql 表列之一生成唯一的随机字符串?

UPPER(HEX(UUID_SHORT()))

为您提供一个唯一的 16 个字符的字母数字字符串。 它有一些不太可能的警告,请参阅https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_uuid-short

“下一个”值通常是可以预测的:

mysql> SELECT UPPER(HEX(UUID_SHORT()));
+--------------------------+
| UPPER(HEX(UUID_SHORT())) |
+--------------------------+
| 161AA3FA5000006          |
+--------------------------+

mysql> SELECT UPPER(HEX(UUID_SHORT()));
+--------------------------+
| UPPER(HEX(UUID_SHORT())) |
+--------------------------+
| 161AA3FA5000007          |
+--------------------------+

转换为 BASE64 可以将字符串减少到 11 个字符:

https://dev.mysql.com/doc/refman/8.0/en/string-functions.html#function_to-base64

mysql> SELECT TO_BASE64(UNHEX(HEX(UUID_SHORT())));
+-------------------------------------+
| TO_BASE64(UNHEX(HEX(UUID_SHORT()))) |
+-------------------------------------+
| AWGqP6UAABA=                        |
+-------------------------------------+

那是 12 个字符,去掉“=”会得到 11 个字符。

这些可能使它不适合您的使用:“下一个”板有点可预测。 字符串中可以有一些标点符号( +/ )。 可能包括小写字母。

DELIMITER $$

USE `temp` $$

DROP PROCEDURE IF EXISTS `GenerateUniqueValue`$$

CREATE PROCEDURE `GenerateUniqueValue`(IN tableName VARCHAR(255),IN columnName VARCHAR(255)) 
BEGIN
    DECLARE uniqueValue VARCHAR(8) DEFAULT "";
    WHILE LENGTH(uniqueValue) = 0 DO
        SELECT CONCAT(SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1)
                ) INTO @newUniqueValue;
        SET @rcount = -1;
        SET @query=CONCAT('SELECT COUNT(*) INTO @rcount FROM  ',tableName,' WHERE ',columnName,'  like ''',@newUniqueValue,'''');
        PREPARE stmt FROM  @query;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
    IF @rcount = 0 THEN
            SET uniqueValue = @newUniqueValue ;
        END IF ;
    END WHILE ;
    SELECT uniqueValue;
    END$$

DELIMITER ;

使用此存储过程并每次都使用它

Call GenerateUniqueValue('tableName','columnName')

生成唯一编号的简单方法

set @i = 0;
update vehicles set plate = CONCAT(@i:=@i+1, ROUND(RAND() * 1000)) 
order by rand();

我一直在寻找类似的东西,我决定制作自己的版本,如果需要,您还可以指定不同的种子(字符列表)作为参数:

CREATE FUNCTION `random_string`(length SMALLINT(3), seed VARCHAR(255)) RETURNS varchar(255) CHARSET utf8
    NO SQL
BEGIN
    SET @output = '';

    IF seed IS NULL OR seed = '' THEN SET seed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; END IF;

    SET @rnd_multiplier = LENGTH(seed);

    WHILE LENGTH(@output) < length DO
        # Select random character and add to output
        SET @output = CONCAT(@output, SUBSTRING(seed, RAND() * (@rnd_multiplier + 1), 1));
    END WHILE;

    RETURN @output;
END

可用作:

SELECT random_string(10, '')

这将使用大小写字符+数字的内置种子。 NULL 也将是值而不是 ''。

但是可以在调用时指定自定义种子:

SELECT random_string(10, '1234')

SQL 触发器是复杂且资源密集型的。 针对基于 MySQL“触发器”的解决方案,这里有一个更简单的解决方案。

  1. 在 MySQL 表列上创建一个 UNIQUE INDEX,该列将保存车辆牌照字符串。 这将确保只有唯一值进入。
  2. 只需在 Lua(或任何其他编程语言,如 ASP、PHP、Java 等)中生成标准的字母数字随机字符串
  3. 使用生成的字符串执行 INSERT 语句,并有错误捕获代码来解析失败(在违反 UNIQUE INDEX 的情况下)
  4. 如果 INSERT 失败,则生成一个新的随机字符串并重新插入。 8 个字符的长度本身很难重复,一旦在表中找到生成另一个字符,几乎不可能再重复。

这将在 DB Server 上更轻、更高效。

这是 PHP 中的示例(伪)代码:

function refercode()
{
    $string = '';
    $characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $max = strlen($characters) - 1;
    for ($i = 0; $i < 8; $i++) {
        $string .= $characters[mt_rand(0, $max)];
    }
    $refer = "select * from vehicles where refer_code = '".$string."' ";
    $coderefertest = mysqli_query($con,$refer);

    if(mysqli_num_rows($coderefertest)>0)
    {
        return refercode();
    }
    else
    {
        return $string;
    }
}
$refer_by = refercode();

这项工作形成了我,生成 6 位数字并在 MySQL 中更新:

产生:

SELECT SUBSTRING(MD5(RAND()) FROM 1 FOR 6)

更新:

UPDATE table_name SET cloumn_name = SUBSTRING(MD5(RAND()) FROM 1 FOR 6) WHERE id = x12

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM