Java实战:分布式ID生成方案

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 在分布式系统的设计与开发过程中,如何生成全局唯一、有序且高可用的ID是一个绕不开的核心问题。尤其是在电商、社交网络、金融交易等领域,ID不仅是业务数据的重要标识,还可能直接影响系统的稳定性和扩展性。本文将深入剖析分布式ID生成方案的设计原则、常见算法,并通过Java示例展示一种可行的实现方式。

在分布式系统的设计与开发过程中,如何生成全局唯一、有序且高可用的ID是一个绕不开的核心问题。尤其是在电商、社交网络、金融交易等领域,ID不仅是业务数据的重要标识,还可能直接影响系统的稳定性和扩展性。本文将深入剖析分布式ID生成方案的设计原则、常见算法,并通过Java示例展示一种可行的实现方式。

一、分布式ID生成的需求分析

全局唯一性:在分布式环境下,必须保证生成的ID在全球范围内不重复,避免数据冲突。

趋势递增:在许多业务场景下,ID有序有助于数据的排序、分页查询以及时间序列分析。

高可用性:ID生成服务需要具备高可用性,即使在部分节点故障的情况下也能继续生成ID。

性能高效:ID生成操作应足够快,尽量降低对业务的影响,尤其在高并发场景下。

易于扩展:随着业务发展,ID生成服务需要能够平滑地进行水平扩展。

二、分布式ID生成方案概述

雪花算法(Snowflake)

雪花算法是一种经典的分布式ID生成方案,由Twitter开源。其ID结构分为64位,由时间戳(41位)、机器标识符(10位)、序列号(12位)组成。通过这种方式,既保证了ID的全局唯一性,又实现了趋势递增。

public class SnowflakeIdWorker {

   private static final long EPOCH = 1577808000000L; // 自定义起始时间戳

   private static final long SEQUENCE_BITS = 12L; // 序列号位数

   private static final long WORKER_ID_BITS = 10L; // 工作节点位数

   private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

   private static final long WORKER_ID_LEFT_SHIFT = SEQUENCE_BITS;

   private final long workerId; // 工作节点ID

   private long sequence = 0L; // 序列号

   private long lastTimestamp = -1L; // 上次生成ID的时间戳

   public synchronized long nextId() {

       long timestamp = timeGen();

       if (timestamp < lastTimestamp) {

           throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));

       }

       if (lastTimestamp == timestamp) {

           // 同一毫秒内,序列号自增

           sequence = (sequence + 1) & ((1 << SEQUENCE_BITS) - 1);

           if (sequence == 0) {

               // 序列号溢出,阻塞等待下一毫秒

               timestamp = tilNextMillis(lastTimestamp);

           }

       } else {

           // 不同毫秒内,序列号重置

           sequence = 0L;

       }

       lastTimestamp = timestamp;

       return ((timestamp - EPOCH) << TIMESTAMP_LEFT_SHIFT) | (workerId << WORKER_ID_LEFT_SHIFT) | sequence;

   }

   // 获取当前时间戳,如果当前时间小于上一次ID生成的时间戳,那么一直循环等待直到超过那个时间戳为止

   protected long tilNextMillis(long lastTimestamp) {

       long timestamp = timeGen();

       while (timestamp <= lastTimestamp) {

           timestamp = timeGen();

       }

       return timestamp;

   }

   // 获取以毫秒为单位的当前时间

   protected long timeGen() {

       return System.currentTimeMillis();

   }

}


UUID

虽然UUID天生具有全球唯一性,但由于其无序性和较长的长度,一般不推荐用于需要趋势递增的场景。但在某些特殊场景下,如临时唯一标识符,可以考虑使用UUID。

数据库自增ID

通过数据库的自增主键来生成ID,简单易行,但存在单点风险、扩展困难等问题,不适合大规模分布式系统。

Zookeeper、Redis等中间件辅助生成

利用中间件的原子操作特性,如Zookeeper的顺序节点、Redis的INCR命令等,可以实现分布式的有序ID生成。不过同样要考虑中间件本身的高可用性和性能瓶颈。

三、分布式ID生成服务的高可用与扩展性设计

高可用性设计:可通过冗余部署多个ID生成服务,每个服务拥有唯一的workId,通过负载均衡器将请求均匀分发到各个服务节点。当某个节点故障时,剩余节点仍然可以继续提供服务。

扩展性设计:增加新的ID生成服务节点时,只需为其分配一个新的workId即可。在雪花算法中,预留足够的workerId位数,就可以支持大量节点的扩展。

四、实战优化与思考

防止单点故障:除了服务冗余外,还可以结合中间件的特性,如Redis哨兵或集群模式,增强服务的容错性。

性能优化:对于雪花算法,可以预先批量生成一批ID并缓存起来,避免每次请求都进行CPU密集型的ID生成操作。

业务连续性保障:设计合理的workId分配策略,确保在扩容或缩容时不影响ID的生成,例如采用时间窗口内轮转分配workerId的方式。

ID的安全性与合规性:遵循ID生成策略的透明性和可追溯性,考虑ID中是否包含敏感信息,以及是否符合法律法规的要求。

五、分布式ID生成的挑战及应对

时钟回拨问题:雪花算法依赖于精确的时间戳,时钟同步问题可能导致ID冲突或生成暂停。为解决此问题,可以引入逻辑时钟或者设置一定的容忍度,在时钟轻微回拨时依然允许ID生成,同时报警提醒运维人员及时处理时钟同步问题。

序列号耗尽:在雪花算法中,每台服务器每毫秒最多能生成 (2^{12}) 个ID,若某节点在一个毫秒内的请求量远超此值,会导致序列号耗尽。针对这一情况,可以通过提前预警机制监控每台服务器的序列号消耗速率,必要时可动态调整工作节点的数量或增大序列号位数。

六、基于Zookeeper实现分布式ID生成器

借助Zookeeper的有序节点特性,我们可以构建一个简单的分布式ID生成服务:

import org.apache.zookeeper.*;

import org.apache.zookeeper.data.Stat;

import java.util.concurrent.CountDownLatch;

public class ZookeeperIdGenerator implements Watcher {

   private static final String ROOT_PATH = "/distributed_ids";

   private ZooKeeper zooKeeper;

   private CountDownLatch latch = new CountDownLatch(1);

   private String currentNodePath;

   public void init(String zkServers) throws Exception {

       zooKeeper = new ZooKeeper(zkServers, 3000, this);

       latch.await();

   }

   @Override

   public void process(WatchedEvent event) {

       if (event.getState() == Event.KeeperState.SyncConnected) {

           latch.countDown();

       }

   }

   public long generateId() throws KeeperException, InterruptedException {

       // 创建一个有序子节点

       currentNodePath = zooKeeper.create(ROOT_PATH + "/id-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);

     

       // 获取当前节点名,去除父路径得到ID

       int idIndex = currentNodePath.lastIndexOf("/") + 1;

       String nodeId = currentNodePath.substring(idIndex);

       // 转换字符串ID为整型ID(假设节点名称直接对应整数值)

       long id = Long.parseLong(nodeId);

       // 返回生成的ID

       return id;

   }

   public void close() throws InterruptedException {

       zooKeeper.close();

   }

   // 省略异常处理及实际项目中的封装代码...

}



在这个示例中,每次生成ID时都会在Zookeeper的指定路径下创建一个有序的持久化节点,节点名称即为ID。由于Zookeeper会自动为这些节点编号,因此保证了ID的全局唯一性和有序性。

然而,这种方案在高并发场景下可能会面临Zookeeper连接压力大、写入速度受限的问题,此时就需要根据实际业务需求进行相应的优化,比如批量获取ID、引入队列等方式。

七、总结

选择和设计分布式ID生成方案是一项细致而重要的任务,不仅要考虑到系统的当前规模,更要预见未来可能的增长趋势。从长远看,一个优秀的分布式ID生成服务应兼具可靠、高效、可扩展的特点,同时还能灵活适应不断变化的业务需求。通过深入理解和实践上述多种方法,我们能够在真实环境中找到最契合自身业务场景的最佳解决方案。

相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
相关文章
|
6月前
|
消息中间件 缓存 分布式计算
java分布式的实现
java分布式的实现
43 0
|
3月前
|
存储 Java 调度
Java基础选填,名词解释题(一)
Java基础选填,名词解释题(一)
14 0
|
消息中间件 NoSQL Java
Java中分布式概念
将一个大的系统划分为多个业务模块,业务模块分别部署到不同的机器上,各个业务模块之间通过接口进行数据交互。区别分布式的方式是根据不同机器不同业务。 微服务的设计是为了不因为某个模块的升级和BUG影响现有的系统业务。 微服务与分布式的细微差别是,微服务的应用不一定是分散在多个服务器上,可以是同一个服务器。
139 0
|
11月前
|
JSON 分布式计算 Java
【案例实战】Java整合hudi-client 0.11.1
【案例实战】Java整合hudi-client 0.11.1
【案例实战】Java整合hudi-client 0.11.1
|
Java
Java—优化 if-else 代码的 8 种方案
Java—优化 if-else 代码的 8 种方案
422 0
Java—优化 if-else 代码的 8 种方案
|
存储 算法 安全
【Java原理探索】带你实战使用String的功能特性 | Java开发实战
【Java原理探索】带你实战使用String的功能特性 | Java开发实战
110 0
【Java原理探索】带你实战使用String的功能特性 | Java开发实战
|
Java
java 大数处理方案指南
1.Java大数值处理方案 如果基本的整数和浮点数精度不能够满足需求,那么可以使用java.math包中的两个很有用的类:BigInteger和BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger类实现了任意精度的整数运算,BigDecimal实现了任意精度的浮点数运算 使用静态的valueOf方法可以将普通的数值转换为大数值:
181 0
|
NoSQL Java 关系型数据库
Java基础内容之分布式锁
在单机环境下多线程操作共享数据时候回用到锁的概念,因为是单机可以直接使用jdk提供的锁机制就可以满足。 但是在微服务场景下,因为是多服务共享数据,此时jdk提供的锁就不能再使用了。于是乎就有了分布式锁。 本文介绍常见的几种可以使用的生产的分布式锁
156 0
|
消息中间件 存储 缓存
Java高性能系统缓存的最佳实践(上)
Java高性能系统缓存的最佳实践
234 0
Java高性能系统缓存的最佳实践(上)
|
存储 消息中间件 缓存
Java高性能系统缓存的最佳实践(下)
Java高性能系统缓存的最佳实践
270 0