利用62进制生成15位UUID(mysql)

最近做物联网平台,需要用到uuid,而需要把uuid烧入到硬件上,所以硬件工程师和我说尽量把UUID缩短点,于是通过这两天的实验,得出了以下方法:

首先,我原本使用的是mysql 的uuid()函数生成形如下面的32位uuid:

68037924-1d62-11e6-a5f0-000c294db5d8

7be35814-1d62-11e6-a5f0-000c294db5d8

1【无损压缩-22位UUID】uuid就是5个16进制整数,既然是整数,那么就可以用进制转换来不损失精度的压缩字符串长度,16进制只使用了’0’~’9′,’a’~’f’这16个字符,这样岂不是很浪费,所以我们可以用’0’~’9′,’a’~‘Z’,’A’~’Z’这62个字符来标识62进制,考虑最坏情况这32个整数是最大值(即都是16进制的f),此时就是这个uuid转换成62进制的最大长度,通过计算得到这个长度是22位。

这样我们成功的把32位UUID无损的转换到了22位,总的可能性是不变的(32位16进制有3.4*10^38个,而22位62进制有2.7*10^39个,但是也只能用到转换前的可能性)。这是使用__int128或者大数运算才能得到的,由于32个f的16进制数正好是2^128-1,如果在不使用大数运算的情况下,我们可以把一个32位UUID拆分成两个16位数,而16个f的16进制数正好是2^64-1,可以用unsigned long long直接运算,而转换后总长度呢?单个16位16进制数最多转换成一个11位的62进制数,所以总的长度还是22位。如果增加进制数(/字符集)可以把位数降的更低,但是出于UUID的可读性等问题,这里就没有增加其他字符了。

2【损失一点精度?15位UUID!】继续观察发现mysql这个uuid最后12位是本机MAC地址,而现在数据库只有一台服务器有,导致最后12位始终不会改变,这样就没有了存在的意义,而再前面4位也是每次重启mysql才会改变的,所以把这4位也删除,这样只有16位了?但是前面16位是时间戳,高并发下会存在重复的概率,所以这里考虑增加5位随机码。而uuid是16进制的数,所以这5位随机码只要在’0’~’9′,’a’~’f’即可。这样就变成了21位的uuid,总共的可能性降低到了16^21=1.9*10^25,个人认为这个可能性在单系统上已经完全足够了。而且前面用mysql的uuid函数在单机上其实也只有16^20的可能性。反而是略微提高了可能性,即使实在上千个结点的分布式系统上也只是略微降低了可能性。

这个21位16进制同样可以用进制转换无损的压缩到15位。这样15位UUID就诞生了!

下面给出mysql存储过程的代码:(发现mysql存储过程好慢,生成1000个需要1秒多,再linux C++上只需要90毫秒,但考虑到C++生成出来还是需要插入数据库,所以最终还是用了存储过程,据说oracle数据库存储过程要快,以后用户起来了再修改优化吧)

BEGIN
	/*生成15位UUID*/
	DECLARE mp char(62);#进制字典
	DECLARE byte char(15);#最后输出62进制
	DECLARE byte2 char(4);#5位62进制随机码
	DECLARE str char(5);#后5位16进制随机码的字符串
	DECLARE i int UNSIGNED;
	DECLARE cnt int UNSIGNED;
	DECLARE num BIGINT UNSIGNED;

	/*获取uuid()*/
	set mp = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
	set num = conv(substring(replace(uuid(), '-', ''),1,16),16,10);

	/*转换成62进制-作为前11位,高位在后*/
	set byte = "";
	set cnt = 0;
	repeat
		set byte = CONCAT(byte,substr(mp,num%62+1,1));
		set num = floor(num/62);
		set cnt = cnt+1;
	until num = 0
	end repeat;
	/*不足11位则补0*/
	while cnt < 11 DO
		set byte = CONCAT(byte,substr(mp,1,1));
		set cnt = cnt+1;
	end while;
	/*生成后5位随机码*/
	set str = "";
	set i = 1;
	while i <= 5 do
		set str = CONCAT(str,substr(mp,floor(rand()*1000)%16+1,1));
		set i = i+1;
	end while;
	SELECT conv(str,16,10) INTO num;
	/*后5位转换成62进制添加在byte后面,高位在前*/
	set byte2 = "";
	set cnt = 0;
	repeat
		set byte2 = CONCAT(substr(mp,num%62+1,1),byte2);
		set num = floor(num/62);
		set cnt = cnt+1;
	until num = 0
	end repeat;
	/*不足4位补0*/
	while cnt < 4 DO
		set byte2 = CONCAT(substr(mp,1,1),byte2);
		set cnt = cnt+1;
	end while;
	/*把前后两块合并*/
	set byte = CONCAT(byte,byte2);
	RETURN byte;
END

小优化:

其实上面生产的5位随机码完全可以在62进制下生成4位0~61的随机码,这样就省去了后5位进制转换,在mysql存储过程1000次可以在0.9秒完成了。

BEGIN
	/*生成15位UUID*/
	DECLARE mp char(62);#进制字典
	DECLARE byte char(15);#最后输出62进制
	DECLARE i int UNSIGNED;
	DECLARE cnt int UNSIGNED;
	DECLARE num BIGINT UNSIGNED;

	/*获取uuid()*/
	set mp = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
	set num = conv(substring(replace(uuid(), '-', ''),1,16),16,10);

	/*转换成62进制-作为前11位,高位在后*/
	set byte = "";
	set cnt = 0;
	repeat
		set byte = CONCAT(byte,substr(mp,num%62+1,1));
		set num = floor(num/62);
		set cnt = cnt+1;
	until num = 0
	end repeat;
	/*不足11位则补0*/
	while cnt < 11 DO
		set byte = CONCAT(byte,substr(mp,1,1));
		set cnt = cnt+1;
	end while;
	/*生成后4位随机码*/
	set i = 0;
	while i < 4 do
		set byte = CONCAT(byte,substr(mp,floor(rand()*1000)%62+1,1));
		set i = i+1;
	end while;
	RETURN byte;
END

 

发表评论

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

请输入正确的验证码