在分布式系统和大规模应用中,唯一标识符的生成是一个至关重要的问题。Java 提供了多种生成唯一标识符的方法,其中UUID(Universally Unique Identifier)和雪花算法(Snowflake Algorithm)是最常用的技术。本文将详细介绍这两种技术的原理、生成方法及其在实际应用中的场景。
1. 引言
简介:为什么需要唯一标识符
在现代软件架构和数据管理中,能够唯一标识信息资源是至关重要的。唯一标识符(Unique Identifier,简称UID)允许系统在全局范围内区分每一个独立的元素,无论是用户、订单、消息还是任何数据记录。这种标识的唯一性保证了数据的一致性和完整性,避免了数据处理过程中的混淆和错误。
在分布式系统中,例如互联网服务、云基础设施和大规模计算环境,需要跨多个节点、位置和时间区间追踪和管理数据。在这些环境中,生成全局唯一的标识符尤为重要,因为传统的基于单一数据库的自增ID生成策略在这些环境中可能会导致ID冲突。
UUID和雪花算法的重要性
UUID(Universally Unique Identifier)和雪花算法(Snowflake Algorithm)是生成唯一标识符的两种流行方法,它们各有优势,并适用于不同的应用场景。
UUID:UUID是一个16字节的数字,通常以32个十六进制数字表示,并通过特定的版本算法生成。它的主要优点是简单易用,能够在本地生成,无需通过网络交互,从而避免了网络延迟和中断的问题。UUID的生成不依赖于中心数据库或其他外部系统,这使得它非常适合需要高度解耦的系统架构。
雪花算法:雪花算法是由Twitter开发的,用于生成64位的长整型数字作为唯一ID。它结合了机器ID、数据中心ID和时间戳信息,可以在分布式系统中快速生成具有时间顺序性的ID。雪花算法的主要优点是生成ID时考虑了时间顺序,这对于需要维护记录顺序的系统特别有用。
这两种算法的存在和发展,显著提升了现代系统中数据处理的效率和可靠性,使得开发者可以更专注于业务逻辑的实现,而不必担心数据标识和冲突的问题。
2. UUID生成方案
UUID的定义和标准
UUID(Universally Unique Identifier)是一种软件建构的标准,也被称为GUID(Globally Unique Identifier)。UUID的主要目的是让分布式系统中的所有元素,都能有唯一的识别信息,而不需要通过中央控制端来做标识符的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这个方面,UUID的目标与主键的目标是相符合的。
UUID是由一个十六位的数字组成,通过特定的算法进行生成,形如:550e8400-e29b-41d4-a716-446655440000
。
UUID的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的32个字符。
Java中生成UUID的方法
在Java中,可以使用java.util.UUID
类来生成UUID。以下是一个简单的示例:
import java.util.UUID; public class Main { public static void main(String[] args) { UUID uuid = UUID.randomUUID(); System.out.println(uuid.toString()); } }
在这个示例中,UUID.randomUUID()
方法被用来生成一个随机UUID。
UUID的版本差异
UUID标准定义了五种不同的生成方法,或者说五个版本。每个版本的UUID都包含一个4位的版本号,以便我们可以区分生成的UUID使用了哪种方法。
版本1:基于时间的UUID:这种UUID使用了发起UUID生成请求的计算机的MAC地址和当前的时间戳(精确到100纳秒)来生成UUID。由于MAC地址是全球唯一的,所以生成的UUID也是全球唯一的。不过,这种方法会因暴露MAC地址而带来一定的安全风险。
版本4:随机生成的UUID:这种UUID完全由随机数生成,没有时间和硬件的限制,也没有安全性问题。Java的
UUID.randomUUID()
方法就是生成这种UUID。不过,由于是随机生成,所以理论上存在生成的UUID重复的可能,但实际上这种可能性非常非常小。
3. UUID的使用场景
UUIDs 提供了一种高度可靠的方式来生成唯一标识符,这在许多不同的技术场景中都非常有用。
以下是一些典型的使用UUID的场景:
网络系统中的唯一性需求
在网络环境中,尤其是在互联网应用和服务中,需要追踪和区分成千上万的请求和事务。UUID可以为每一个请求或事务生成一个唯一的标识符,确保即使在高并发的情况下也不会产生冲突。例如,Web API可以为每个请求生成一个UUID,用于日志记录、监控和追踪问题,从而提高服务的可靠性和可追溯性。
数据库主键
在数据库设计中,使用UUID作为主键是一种常见的做法,尤其是在分布式数据库系统中。与传统的递增整数主键相比,UUID可以避免跨数据库的同步和冲突问题,使得数据库的扩展更为灵活和可靠。此外,使用UUID作为主键可以减少数据库迁移和合并时的复杂性,因为它保证了即使在不同的数据库间也不会出现主键的重复。
分布式系统中的身份标识
在分布式系统中,尤其是那些涉及多个服务和组件的大型系统中,需要一种机制来唯一标识每个组件或节点。UUID为这些系统提供了一种简单而有效的方式来生成这种唯一标识符。例如,微服务架构中的每个服务实例可以使用UUID来标识,这有助于在服务发现和负载均衡等过程中确保正确的资源分配和管理。
总的来说,UUID的使用可以极大地增强系统的健壮性、可扩展性和安全性。其能够在不依赖中心化控制的情况下生成全局唯一的标识符,使得它成为现代软件和系统设计中不可或缺的一个工具。
4. 雪花算法(Snowflake Algorithm)
雪花算法的介绍
雪花算法(Snowflake Algorithm)是由Twitter开发的一种用于生成唯一ID的算法,特别适用于分布式系统中。
这种算法可以在不需要中央数据库的情况下生成全局唯一的ID,非常适合需要处理大量数据和高并发请求的应用。
结构解析
雪花算法生成的ID是一个64位的整数,这64位中包含了以下几个部分:
时间戳 - 占用41位,精确到毫秒级,可以使用约69年。
数据中心ID - 占用5位,可以有最多32个数据中心。
机器ID - 占用5位,每个数据中心可以有最多32台机器。
序列号 - 占用12位,每个节点每毫秒可以生成最多4096个ID。
这种结构确保了即使在同一时间同一数据中心的同一机器上发生多个请求,生成的ID也是唯一的。
Java实现雪花算法
要在Java中实现雪花算法,我们需要定义一个类来处理ID生成的逻辑。
下面是一个简单的实现示例:
import cn.hutool.core.net.NetUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Random; /** * Twitter_Snowflake<br> * SnowFlake的结构如下(每部分用-分开):<br> * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - * 000000000000 <br> * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br> * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T * = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br> * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br> * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br> * 加起来刚好64位,为一个Long型。<br> * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。 */ @Component public class SnowflakeIdWorker { /** * 开始时间截 (2019-06-21) */ private final long twepoch = 1561104939733L; /** * 机器id所占的位数 */ private final long workerIdBits = 5L; /** * 数据标识id所占的位数 */ private final long datacenterIdBits = 5L; /** * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** * 支持的最大数据标识id,结果是31 */ private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); /** * 序列在id中占的位数 */ private final long sequenceBits = 12L; /** * 机器ID向左移12位 */ private final long workerIdShift = sequenceBits; /** * 数据标识id向左移17位(12+5) */ private final long datacenterIdShift = sequenceBits + workerIdBits; /** * 时间截向左移22位(5+5+12) */ private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; /** * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */ private final long sequenceMask = -1L ^ (-1L << sequenceBits); /** * 工作机器ID(0~31) */ private long workerId = getWorkId(); /** * 数据中心ID(0~31) */ private long datacenterId = getDataId(); /** * 毫秒内序列(0~4095) */ private long sequence = 0L; /** * 上次生成ID的时间截 */ private long lastTimestamp = -1L; /** * 机器随机获取数据中中心id的参数 32 */ private final long DATA_RANDOM = maxDatacenterId + 1; /** * 随机获取的机器id的参数 */ private final long WORK_RANDOM = maxWorkerId + 1; private static final Logger logger = LoggerFactory.getLogger(SnowflakeIdWorker.class); @PostConstruct public void init() { logger.debug("snowflake-work-id:{}", getWorkId()); logger.debug("snowflake-data-id:{}", getDataId()); } public SnowflakeIdWorker() { // this(0, 0); } /** * 构造函数 * * @param workerId 工作ID (0~31) * @param datacenterId 数据中心ID (0~31) */ public SnowflakeIdWorker(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException( String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException( String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } /** * 获得下一个ID (该方法是线程安全的) * * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 if (timestamp < lastTimestamp) { if (lastTimestamp - timestamp < 2000) { // 容忍2秒内的回拨,避免NTP校时造成的异常 timestamp = lastTimestamp; } else { // 如果服务器时间有问题(时钟后退) 报错。 throw new RuntimeException(String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } } // 如果是同一时间生成的,则进行毫秒内序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; // 毫秒内序列溢出 if (sequence == 0) { // 阻塞到下一个毫秒,获得新的时间戳 timestamp = tilNextMillis(lastTimestamp); } } // 时间戳改变,毫秒内序列重置 else { sequence = 0L; } // 上次生成ID的时间截 lastTimestamp = timestamp; // 移位并通过或运算拼到一起组成64位的ID return ((timestamp - twepoch) << timestampLeftShift) // | (datacenterId << datacenterIdShift) // | (workerId << workerIdShift) // | sequence; } /** * 阻塞到下一个毫秒,直到获得新的时间戳 * * @param lastTimestamp 上次生成ID的时间截 * @return 当前时间戳 */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 返回以毫秒为单位的当前时间 * * @return 当前时间(毫秒) */ protected long timeGen() { return System.currentTimeMillis(); } /** * 根据host name 取余,发生异常就获取0到31之间的随机数 * * @return */ public long getWorkId() { try { String ip = NetUtil.getLocalhost().getHostAddress(); logger.info("服务器IP:{}", ip); return getHostId(ip, maxWorkerId); } catch (Exception e) { return new Random().nextInt((int) WORK_RANDOM); } } /** * 根据host name 取余,发生异常就获取0到31之间的随机数 * * @return */ public long getDataId() { try { String ip = NetUtil.getLocalhost().getHostAddress(); logger.info("服务器IP:{}", ip); return getHostId(ip, maxDatacenterId); } catch (Exception e) { return new Random().nextInt((int) DATA_RANDOM); } } /** * 根据host name 取余 * * @return */ private long getHostId(String s, long max) { byte[] bytes = s.getBytes(); long sums = 0; for (byte b : bytes) { sums += b; } return sums % (max + 1); } /** * 测试 */ public static void main(String[] args) { SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); System.out.println(idWorker.nextId()); } }
在这个实现中,我们创建了一个SnowflakeIdWorker
类,该类可以在构造时接受数据中心ID和机器ID,并提供了一个nextId()
方法来生成新的ID。
这个方法确保生成的ID是按时间顺序递增的,并且在多线程环境中是安全的。
5. 雪花算法的使用场景
雪花算法由于其独特的结构和高效的性能特点,非常适用于特定的技术场景。以下是雪花算法的一些主要使用场景:
大规模分布式系统中的ID生成
在大规模分布式系统中,需要确保在多个节点和服务之间生成的ID是唯一的,同时又不能依赖于中央数据库或服务来维护ID的唯一性。雪花算法通过结合时间戳、数据中心ID、机器ID和序列号生成唯一的ID,从而无需进行网络交互即可在各个节点独立生成ID。这种方法非常适合用于电商平台、社交网络、在线游戏等业务,其中需要处理大量数据并且对ID生成的性能要求极高。
性能考量和优势
雪花算法的一个显著优势是其生成ID的速度非常快,可以在毫秒级别生成数百万个ID,这对于高并发的应用场景尤为重要。此外,由于ID是基于时间戳生成的,这自然地保证了生成的ID的顺序性(在同一毫秒内通过序列号保证顺序),这对于需要保持事件顺序的应用场景(如日志记录、消息队列等)非常有用。
雪花算法的另一个优势是其扩展性好。通过调整数据中心ID和机器ID的位数,可以灵活适应不同规模的系统扩展需要。这使得雪花算法不仅适用于大型系统,也适用于中小型系统,甚至是动态扩展的云环境。
总之,雪花算法是解决分布式系统中ID生成问题的一个高效、可靠的解决方案,它通过独特的设计满足了高性能、高可用性和可扩展性的需求。
6. UUID与雪花算法的比较
UUID和雪花算法都是在特定场景下生成唯一ID的有效工具。然而,它们在性能、应用场景和选择依据方面有着显著的差异。
性能比较
UUID:UUID的生成过程非常简单,只需要调用函数即可立即生成。因此,UUID的生成性能很高。然而,UUID的长度较长(32位十六进制),在存储和传输上会占用更多的资源。此外,如果在数据库中使用UUID作为主键,可能会导致索引性能下降。
雪花算法:雪花算法生成的ID长度较短(64位整数),在存储和传输上更加高效。而且,由于雪花算法生成的ID是有序的,因此在数据库中使用雪花算法生成的ID作为主键可以提高索引性能。然而,雪花算法的生成过程比UUID更复杂,需要维护时间戳、数据中心ID、机器ID和序列号等信息。
应用场景差异
UUID:UUID最大的优点是可以在任何地方生成,不需要考虑系统的分布式架构。因此,UUID非常适合在分布式系统中作为全局唯一标识符使用。
雪花算法:雪花算法最大的优点是生成的ID是有序的,非常适合在需要保证顺序的场景中使用。例如,如果需要按照ID的生成顺序进行数据处理,那么雪花算法会是一个更好的选择。
选择依据
在选择UUID和雪花算法时,需要考虑以下几个因素:
系统架构:如果系统是分布式的,并且需要在多个节点上生成唯一ID,那么雪花算法可能是一个更好的选择。如果系统架构较简单,或者不需要在多个节点上生成唯一ID,那么UUID可能是一个更好的选择。
性能需求:如果系统对存储和传输效率有较高的要求,那么应该选择生成长度较短的雪花算法ID。如果系统对生成ID的速度有较高的要求,那么应该选择生成速度较快的UUID。
顺序需求:如果系统需要按照ID的生成顺序进行操作,那么应该选择雪花算法。如果系统不需要保证ID的顺序,那么可以选择UUID。
总结
通过本文的介绍,我们深入探讨了UUID和雪花算法的原理及其在Java中的实现方法。UUID是一种基于随机数和时间戳的全局唯一标识符,适用于跨平台和分布式系统的唯一性要求。而雪花算法则是一种基于时间戳和机器ID的分布式ID生成算法,具有高效性和低碰撞率的特点。希望本文的示例和解释能够帮助读者更好地理解和应用这两种技术,在实际项目中解决唯一标识符生成的问题。
本文来源于#CC大煊,由@蜜芽 整理发布。如若内容造成侵权/违法违规/事实不符,请联系本站客服处理!
该文章观点仅代表作者本人,不代表本站立场。本站不承担相关法律责任。
如若转载,请注明出处:https://www.zhanid.com/biancheng/3005.html