最近做物联网平台,需要用到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