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


110

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

现在是我遇到问题的部分。在创建新车辆之前,我需要找到一个未使用的车牌-它应该是字母数字8字符的随机字符串。我是通过使用Lua(我正在使用的语言)中的while循环生成字符串并查询数据库以查看是否使用它的,从而实现了这一点。但是,随着车辆数量的增加,我希望这会变得目前效率更低。因此,我决定尝试使用MySQL查询解决此问题。

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

任何帮助表示赞赏。


您需要这些随机性如何?如果某人收到特定的车牌,他们能否计算出您下一个或上一个车牌是否重要?
Damien_The_Unbeliever

@YaK关于如何避免发生碰撞的微小可能性,请参阅我的答案
Eugen Rieck

Answers:


87

此问题包含两个非常不同的子问题:

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

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

  • @paul已建议使用哈希
  • AES加密也适合
  • 但是有一个不错的: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;

很有意思的方法,但是您可能是故意的RAND(LAST_INSERT_ID()); UPDATE vehicles (...) , rand()*36+1, (...)(否则它将返回相同字符的8倍)。rand()如果使用不同的种子初始化,我们如何确保对8个连续的调用保证返回不同的序列?
2013年

8
我只是想知道。为什么使用这些数字..4294967296))* 36 + 1?
米克

7
这有点旧,但是我想指出,我必须FLOOR()在第二个子字符串参数周围添加: substring('ABC … 789', floor(rand(@seed:= … )*36+1), 1), 在某些情况下,子字符串试图选择字符36.9,将其舍入为37时,将导致未选择任何字符。
菲利普·多德森

4
如果字符串是可复制的,则不能将其称为随机字符串。由于您正在使用,因此也可以重复floor()。此sqlfiddle显示,为三个字符长的字符串创建了重复项。
Paul Spiegel

6
@EugenRieck我不明白您如何获取数字(“前2 ^ 32次迭代”)。但是我只需要一个重复的例子来证明这一概念。对于ID 193844775771您的算法,将生成相同的字符串T82X711demo)。
Paul Spiegel

113

正如我在评论中指出的那样,我不会担心发生碰撞的可能性。只需生成一个随机字符串,然后检查它是否存在。如果是这样,请再试一次,除非已经分配了大量印版,否则您无需重复进行多次。

在纯(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()(但是我会考虑将此逻辑移至应用程序层)。


感谢您的解决方案,我对UUID有一个问题。您再次生成8个字符的ID的机会有多大。
TR-Ahmed 2014年

1
@ user3099183 正式地,“非常低”。16 ^ 8大约有40亿个字符串。
2014年

23
请注意,UUID的前8个字符不是随机的,而是顺序的,因为它是基于时间戳的。
ADTC

我想生成一个9个字符长的随机字符串,当9在您的代码中使用时SELECT LEFT(UUID(), 9);-在生成的字符串的末尾总是第九个字符。它是恒定的。为什么?
马丁AJ

3
@MartinAJ,因为该字符串是uuid。您可以轻松地替换连字符,例如SELECT LEFT(REPLACE(UUID(), '-', ''), 16);
jchook

53

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

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

等等

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

编辑:这是一个旧的答案,但我又一次看到它,但随着时间的流逝,从观察开始...

所有数字的机会= 2.35%

所有字母的出现几率= 0.05%

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


2
好主意,可以在主键上使用它。在以后的项目中请牢记这一点,谢谢!
funstein

2
可能只有数字会导致这种情况
Mladen Janjetovic

9
它根本不是随机的。
2016年

1
如果使用插入完成的日期时间代替唯一的增量时间,则可以使其更“随机”。
哈维尔·拉·班卡

35

创建一个随机字符串

这是一个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 ;

6
这应该是公认的答案,很明确,很关键,对我来说还不错,谢谢@ paddy-mann
Saif

我认为这是最好的解决方案。谢啦!
Pronoy999

23

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

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;

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


7
使用floor(rand()* 36 + 1)代替。否则,某些结果将是“简短的”。
脆弱

2
应该有35 + 1而不是36 + 1!否则,您将得到一些带有8个字符的字符串,其中一些带有7个字符
BIOHAZARD

23

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

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


16

您可以使用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;

14

您可以使用以下方法生成随机的字母数字字符串:

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,则可以这种方式工作。如果您希望它不为空,则需要定义一个默认值

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

如果您不希望使用大写字母数字,则还可以在触发器中使用任何其他随机字符串生成算法。但是触发器将照顾到唯一性。


惊人!这正是我想要的。我只想了解如何将其转换为字符串。为什么会有战俘,该怎么办,增加数字等等。无论如何,谢谢。
阿克西(Akxe)

@Akxe conv()可用于将数字转换为字母数字字符串。pow(36,8)-1是的数字表示形式ZZZZZZZZ。因此,我们生成一个随机整数之间0和'36 ^ 8-1' (从02821109907455),并转换成之间的字母数字字符串0ZZZZZZZZunsing conv()拉帕德()将用零填充字符串直到其具有8的长度
保罗明镜

先生,你是个天才。我认为由于字符的不连续性,不可能添加小写字母吗?(91-96)并不是我需要它,只是好奇...
Akxe

@Akxe conv()仅支持不超过36(10位数字+ 26个大写字母)的基数。如果要包含小写字母,则需要另一种方式将数字转换为字符串。
Paul Spiegel

警告:不适用于str_len>13。从14开始,您始终会获得“ 3W5E11264SGSF”。;-)
Gerard H. Pille

6

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

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

您收到这样的信息:

353E50CC


5

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

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

由内而外的解释:

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

太好了,正是我想要在MySQL中本地创建类似令牌的ID的内容
rabudde

4

我使用另一列中的数据来生成“哈希”或唯一字符串

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

4

字母表中的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))));

3

如果您没有ID或种子,例如在插入中的值列表中使用其ID或种子:

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

2

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

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

1

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


1

考虑到所需的字符总数,生成两个完全相似的车牌的机会很小。因此,您可能可以避免在LUA中生成数字。

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

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


没错,我会在实际游戏中继续使用它而不是SQL。非常感谢你。
funstein

1

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

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 ;

该代码基于“罗斯·史密斯二世”发送的随机播放字符串函数


此函数将不会生成随机唯一值。
Faisal

1

要创建一个随机的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的答案以获取有关此代码的工作方式的细分。


0
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')

0

一种生成唯一编号的简单方法

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


0

我在寻找类似的东西,因此决定制作自己的版本,如果需要(字符列表)作为参数,也可以指定其他种子:

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')
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.