在分布式系统中,往往需要对大量的数据和消息进行唯一标识,此时一个能够生成全局唯一ID的系统是非常必要的,那么业务系统对ID号的要求有哪些呢?
- 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
- 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
- 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
UUID
UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:5e8c4456-6166-40d6-9b9f-fb37a150bc6e,到目前为止业界一共有5种方式生成UUI,Java标准类库中已经提供了UUID的API。
UUID.randomUUID()
优点:
- 性能非常高:本地生成,没有网络消耗。
缺点:
- 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
- 主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。
类snowflake方案
雪花ID生成的是一个64位的二进制正整数,然后转换成10进制的数。64位二进制数由如下部分组成:
41-bit的时间可以表示(1L<<41)/(1000L*3600*24*365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。
但是对于绝大部分普通应用程序来说,根本不需要每秒超过400万的ID,机器数量也达不到1024台,所以,我们可以改进一下,使用更短的ID生成方式:53bitID由32bit秒级时间戳+16bit自增+5bit机器标识组成,累积32台机器,每秒可以生成6.5万个序列号。
代码示例
/** * 53 bits unique id: * * |--------|--------|--------|--------|--------|--------|--------|--------| * |00000000|00011111|11111111|11111111|11111111|11111111|11111111|11111111| * |--------|---xxxxx|xxxxxxxx|xxxxxxxx|xxxxxxxx|xxx-----|--------|--------| * |--------|--------|--------|--------|--------|---xxxxx|xxxxxxxx|xxx-----| * |--------|--------|--------|--------|--------|--------|--------|---xxxxx| * * Maximum ID = 11111_11111111_11111111_11111111_11111111_11111111_11111111 * * Maximum TS = 11111_11111111_11111111_11111111_111 * * Maximum NT = ----- -------- -------- -------- ---11111_11111111_111 = 65535 * * Maximum SH = ----- -------- -------- -------- -------- -------- ---11111 = 31 * * It can generate 64k unique id per IP and up to 2106-02-07T06:28:15Z. */public class IdUtil { private static final Logger logger = LoggerFactory.getLogger(IdUtil.class); private static final Pattern PATTERN_LONG_ID = Pattern.compile("^([0-9]{15})([0-9a-f]{32})([0-9a-f]{3})$"); private static final Pattern PATTERN_HOSTNAME = Pattern.compile("^.*D+([0-9]+)$"); private static final long OFFSET = LocalDate.of(2000, 1, 1).atStartOfDay(ZoneId.of("Z")).toEpochSecond(); private static final long MAX_NEXT = 0b11111_11111111_111L; private static final long SHARD_ID = getServerIdAsLong(); private static long offset = 0; private static long lastEpoch = 0; public static long nextId() { return nextId(System.currentTimeMillis() / 1000); } private static synchronized long nextId(long epochSecond) { if (epochSecond < lastEpoch) { // warning: clock is turn back: logger.warn("clock is back: " + epochSecond + " from previous:" + lastEpoch); epochSecond = lastEpoch; } if (lastEpoch != epochSecond) { lastEpoch = epochSecond; reset(); } offset++; //如果offset=65536 next=0 long next = offset & MAX_NEXT; if (next == 0) { logger.warn("maximum id reached in 1 second in epoch: " + epochSecond); //向下一秒借65535个 return nextId(epochSecond + 1); } return generateId(epochSecond, next, SHARD_ID); } private static void reset() { offset = 0; } private static long generateId(long epochSecond, long next, long shardId) { return ((epochSecond - OFFSET) << 21) | (next << 5) | shardId; } private static long getServerIdAsLong() { try { String hostname = InetAddress.getLocalHost().getHostName(); Matcher matcher = PATTERN_HOSTNAME.matcher(hostname); if (matcher.matches()) { long n = Long.parseLong(matcher.group(1)); if (n >= 0 && n < 8) { logger.info("detect server id from host name {}: {}.