UUID生成策略/算法(JavaScript / Typescript)
-
我只是想简单的生成一些UUID,不想引入一堆库,所以最后设计了一个简单的UUID生成策略。
-
下面的UUID生成方法是依据时间戳、随机数、count计数器三个输入因子生成的,能保证本地的UUID是不会重复的,但是在多用户端环境下有非常非常低的概率出现重复。
-
多用户端场景UUID有可能重复的极端场景,需要满足以下两个前提条件:
- 两个用户端在同一毫秒内完成了此代码的初始化(获得了相同的时间戳)。
- 两个用户生成的随机数相同(理论上相同的概率是一百亿分之一)。
实现代码(JavaScript / Typescript):
// UUID中允许出现的基本字符;
const baseChar: string[] = (() => {
const array: string[] = [];
// 将数字0-9加入到 baseChar 中
for (let i = 0; i < 10; i++) {
array.push(i.toString(10));
}
// 将小写字母a-z加入到 baseChar 中
for (let i = 0; i < 26; i++) {
array.push(String.fromCharCode('a'.charCodeAt(0) + i));
}
// 将大写字母A-Z加入到 baseChar 中
for (let i = 0; i < 26; i++) {
array.push(String.fromCharCode('A'.charCodeAt(0) + i));
}
return array;
})();
// 进制转换/数字转字符
function scaleTransition(value: number, base = baseChar): string {
if (value < 0) {
throw new Error('scaleTransition Error, value < 0');
}
if (value === 0) {
return base[0];
}
const radix = base.length;
let result = '';
let resValue = value;
while (resValue > 0) {
result = base[resValue % radix] + result;
resValue = Math.floor(resValue / radix);
}
return result;
}
// UUID参数集合, 使用 count 能保证在本地环境中UUID绝对不会重复.
const UuidObject: Record<string, { prefix: string; count: number }> = {};
// 生成随机的UUID
export default function createUUID(prefix = '') {
let uuidObject = UuidObject[prefix];
if (uuidObject === undefined) {
const str = `${scaleTransition(Date.now())}-${scaleTransition(
Math.floor(Math.random() * 10000000000),
)}-`;
if (prefix) {
uuidObject = {
prefix: `${prefix}-${str}`,
count: 0,
};
} else {
uuidObject = {
prefix: str,
count: 0,
};
}
UuidObject[prefix] = uuidObject;
}
return uuidObject.prefix + scaleTransition(uuidObject.count++);
}
- 这里的baseChar中包含的是UUID中允许出现的字符。
- scaleTransition会将传入的value进行“进制转换”。
- 总得来说,baseChar其中包含的字符越多,最后生成的UUID长度越短。
进阶版,加入服务端前缀与MD5散列
import MD5 from 'md5';
// UUID中的分隔符
const splitChar = '.';
// UUID中允许出现的基本字符;
const baseChar: string[] = (() => {
const array: string[] = [];
// 将数字0-9加入到 baseChar 中
for (let i = 0; i < 10; i++) {
array.push(i.toString(10));
}
// 将小写字母a-z加入到 baseChar 中
for (let i = 0; i < 26; i++) {
array.push(String.fromCharCode('a'.charCodeAt(0) + i));
}
// 将大写字母A-Z加入到 baseChar 中
for (let i = 0; i < 26; i++) {
array.push(String.fromCharCode('A'.charCodeAt(0) + i));
}
// 加入一些其它的ASCII中的打印字符(尽量对URL或者JSON友好)
array.push('*', '+', '-', '<', '=', '>');
array.push('(', ')', '[', ']', '{', '}', '|');
array.push('!', '$', '%', ':', ';', '@', '^', '_', '~');
return array;
})();
// 进制转换/数字转字符
function scaleTransition(value: number | bigint, base = baseChar): string {
if (value < 0) {
throw new Error('scaleTransition Error, value < 0');
}
if (value === 0) {
return base[0];
}
if (typeof value === 'bigint') {
const radix = BigInt(base.length);
let result = '';
let resValue = value;
while (resValue > 0) {
result = base[Number(resValue % radix)] + result;
resValue /= radix;
}
return result;
} else {
const radix = base.length;
let result = '';
let resValue = value;
while (resValue > 0) {
result = base[resValue % radix] + result;
resValue = Math.floor(resValue / radix);
}
return result;
}
}
function getPrefix(): string {
// TODO 从服务端UUID前缀, 来避免多用户端生成的uuid出现重复, (如果获取失败,则使用时间戳来兜底)
return String(Date.now());
}
// UUID参数集合, 使用 count 能保证在本地环境中UUID绝对不会重复.
const UuidObject: Record<string, { prefix: string; count: number }> = {};
function getUuidInfo(prefix: string): { prefix: string; count: number } {
let uuidObject = UuidObject[prefix];
if (uuidObject === undefined) {
// 先使用MD5将 getPrefix 散列成为一个32位长度的16进制字符串, 再将其转换为bigInt数值, 最后使用scaleTransition转换来压缩长度
const str1 = scaleTransition(BigInt(`0x${MD5(getPrefix())}`));
// MD5散列理论上存在碰撞的可能性, 可以在加入一个一亿以内的随机数降低在散列发生碰撞时生成UUID重复的可能性
const str2 = scaleTransition(Math.floor(Math.random() * 100000000));
if (prefix) {
uuidObject = {
prefix: prefix + splitChar + str1 + splitChar + str2 + splitChar,
count: 0,
};
} else {
uuidObject = {
prefix: str1 + splitChar + str2 + splitChar,
count: 0,
};
}
UuidObject[prefix] = uuidObject;
}
return uuidObject;
}
// 生成随机的UUID
export default function createUUID(prefix = '') {
const uuidObject = getUuidInfo(prefix);
return uuidObject.prefix + scaleTransition(uuidObject.count++);
}
// for (let i = 0; i < 10; i++) {
// console.log(createUUID());
// }