实战:10 分钟掌握分布式 ID 之雪花算法
一个在生产每天经过1亿+数据量验证的id生成器
背景
1.为什么要使用雪花算法生成 ID
-- 保证 id 全局唯一
-- 保证 id 自增长
-- uuid 无序且过长
雪花算法 ID 组成
1: 1位标识部分:
--- 在 java 中由于 long 的最高位是符号位,正数是 0,负数是 1,一般生成的 ID 为正数,所以为 0;
2: 41 位时间戳部分:
--- 这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的 ID 从更小值开始;41 位的时间戳可以使用 69 年,(1L<< 41) / (1000L _ 60 _ 60 _ 24 _ 365) = 69 年;
3: 10 位workid:
Twitter 实现中使用前 5 位作为数据中心标识,后 5 位作为机器标识,可以部署 1024 个节点。我这里的实现根据服务名生产的,意思就是说每个服务只要不超过 1024 个节点就不会有问题,实际生产中我也没有见过某个服务有 1024 个节点的。
4: 12 位序列号部分:
--- 支持同一毫秒内同一个节点可以生成 4096 个 ID。意思就是说某个服务 1ms 能生成 4096 个 id,如果你单表的 TPS 超过 4096\*60s,那可能就会出问题了,实际生产这么大的 TPS 我是没有见过的。
Zookeeper生成workid
雪花算法生成ID网络上方法很多,很多重复的东西我就不赘述了,这里简明扼要的说一下ZK生成workid。
这是生成的ID的截图。父节点是workid,只要有引用id生成器jar的服务都会在workid下面生成一个文件夹,以服务名命名,有多少个节点该节点下就会有多个文件,该节点下的id一定是不会重复的。下面我贴一下java版生成workid的核心代码
private void buildWorkId(final String appPath) { // 检测client是否已经连接上 if (null == client) { throw new RuntimeException("本节点注册到ZK异常。"); } // lockPath,用于加锁,注意要与nodePath区分开 final String lockPath = this.ROOT_NAME + "/" + this.appName; // nodePath 用于存放集群各节点初始路径 final String nodePath = this.ROOT_NAME + "/" + this.appName + this.NODE_NAME;// InterProcessMutex 分布式锁(加锁过程中lockPath会自动创建) InterProcessLock interProcessLock = new InterProcessMutex(client, lockPath); try { if (!interProcessLock.acquire(5000, TimeUnit.MILLISECONDS)) { throw new TimeoutException("ZK分布式锁 加锁超时,超时时间: " + 5000); } // nodePath 第一次需初始化,永久保存, 或者节点路径为临时节点,则设置为永久节点 if (null == client.checkExists().forPath(nodePath)) { client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(nodePath); } // 获取nodePath下已经创建的子节点 List childPath = client.getChildren().forPath(nodePath); Set nodeIdSet = new LinkedHashSet<>(); if (!CollectionUtils.isEmpty(childPath)) { for (String path : childPath) { try { nodeIdSet.add(Integer.valueOf(path)); } catch (Exception e) { log.warn("路径由不合法操作创建,注意[" + nodePath + "]仅用于构建workId"); } } } // 遍历所有id,构建workId,主要是判断可用id是否已经被集群中其他节点占用 for (Integer order : OrderIdSet) { if (!nodeIdSet.contains(order)) { final String currentNodePath = nodePath + "/" + order; String nodeDate = String.format("[ip:%s,hostname:%s,pid:%s]", InetAddress.getLocalHost().getHostAddress(), InetAddress.getLocalHost().getHostName(), ManagementFactory.getRuntimeMXBean().getName().split("@")[0]); try { client.create().withMode(CreateMode.EPHEMERAL).forPath(currentNodePath, nodeDate.getBytes("UTF-8")); } catch (Exception e) { log.debug("节点[{}]无法创建,可能是已存在", currentNodePath); continue; } long pathCreateTime = client.checkExists().forPath(currentNodePath).getCtime(); // 以下逻辑主要用于检测断开重连情况 TreeCache treeCache = new TreeCache(client, currentNodePath); // 添加监听器 treeCache.getListenable().addListener(new TreeCacheListener() { public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception { long pathTime; try { pathTime = curatorFramework.checkExists().forPath(currentNodePath).getCtime(); } catch (Exception e) { pathTime = 0; } // 如果pathTime != pathCreateTime, 那么只能一种情况: // 当前应用与zk失去联系,且/nodePath/{currentNodePath}不存在或者被其它应用占据了(表象为pathCreateTime变化) // 无论哪种情况,当前应用都要重新注册节点 if (pathCreateTime != pathTime) { log.info("从ZK断开,再次注册..."); // 关闭之前旧的treeCache try { treeCache.close(); } catch (Exception e) { log.warn("treeCache关闭失败"); } // 再次注册 finally { buildWorkId(appPath); } } } }); treeCache.start(); workerId = order; log.info("基于ZK成功构建 workId:{}", workerId); return; } } } catch (Exception e) { e.printStackTrace(); log.error("获取分布式WorkId异常", e); } finally { // 构建成功后释放锁 if (interProcessLock != null) { try { interProcessLock.release(); } catch (Exception e) { log.warn("释放锁失败"); } } } }
核心代码我已经贴出来了,对雪花算法有一定了解的同学,使用的时候在需要id生成器的地方引用就好了,还有部分非核心代码,这个ID生成器已经在生产验证了每天1亿+的数据量,如果你真的需要可以联系我,把代码都给你。zookeeper其实是一个很实用的工具,还有分布式锁的实现及应用,下篇文章会给大家带来用zk生成分布式锁。
END
、