前言 为了加快数据库的查询速度,主键和索引的选择也是相当重要的一部分,此文介绍几个常见的分布式 id 生成方案,并讲述几种雪花算法的实现形式。经过综合权衡,当数据量较大时,使用雪花算法 生成分布式 id 能够在确保安全性 的同时保持较优的查询性能 ,是一种比较适合目前实际应用的 id 生成算法。
分布式 ID 基本规则
全局唯一性: 不能出现重复的 id
递增性: MySQL 的 InnoDB 使用的是聚簇索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,因此在主键的选择上我们还是应该尽可能地使用有序的主键来保证写入性能,我们保证下一个 id 一定大于上一个 id,以此来满足事务版本号、IM 增量消息或者排序的特殊需求
安全性: 如果 id 是连续的,那么我们在知道一些基本规则的情况下就能很轻松地推测出下一份数据,这在一些机密性较高的业务场景是很危险的。所以我们有时会希望 id 是无规则的,最好还能包含有时间戳,这样就能够在开发中快速了解这个分布式 id 的生成时间
高性能高可用性: 确保在任何时候都能正确地生成 id,并且在高并发的环境下也能表现良好
常见分布式 ID 方案 UUID(Universally Unique Identifier ) 核心思想: 基于机器网卡、当地时间和随机数生成 UUID。
优点
全局唯一: UUID 可以为分布式系统提供全局唯一的标识符,避免了 id 冲突的问题;
生成简单: UUID 的生成不需要中央协调机制,因此可以在任何时间和地点生成 UUID;
安全性高: UUID 的生成使用了随机性或伪随机性的元素,生成的 UUID 具有高度随机性,不容易被猜测到。因此可以在安全领域中使用。
缺点
存储空间大 :存储 UUID 值(16 字节)比整数(4 字节)甚至大整数(8 字节)需要更多的存储空间;
性能问题 :UUID 的生成算法比较复杂,在一定程度上影响了性能;
无序查询效率低: UUID 由于自身的随机性,每次插入新记录时,新的记录可能会被插入到索引树的不同位置,导致索引的不连续性,会造成索引叶子结点频繁分裂、合并,降低了查询性能,不适合作为索引。
数据库自增 ID 核心思想: 使用数据库的自增字段,如 MySQL 的 auto_increment
。
优点
简单: 易于实现,数据库系统可以直接生成;
占用空间小: 自增 id 通常存储为整数,占用空间小;
天然有序: 自动增长,有序索引效率高。
缺点
可预测性 :自增 id 的可预测性可能导致安全问题,如通过 id 猜测其他记录的存在;
分布式系统中的问题 :在分布式系统中,多个数据库实例可能会产生冲突的自增 id。
雪花算法 核心思想: 由 Twitter 开发,使用时间戳、数据中心 id、机器 id 和序列号生成 64 位 id。
优点
趋势递增: 毫秒数在高位,自增序列在低位,整个 id 都是趋势递增的;
生成简单: 可以不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成 id 的性能也非常高;
灵活: 可以根据自身业务特性分配 bit 位,非常灵活。
缺点
强依赖机器时钟: 如果机器上时钟回拨,会导致发号重复或者服务处于不可用状态。
雪花算法原理
雪花算法共 64 bit,具体包括:
其中,数据中心 id 与机器 id 可以合并作为 10 bit 的机器 id 使用。
实现方法 自定义实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 public class SnowflakeUtil { private static final long START_STAMP = 1420041600000L ; private static final long WORKER_ID_BITS = 5L ; private static final long DATACENTER_ID_BITS = 5L ; private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); private static final long SEQUENCE_BITS = 12L ; private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS; private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); private static long workerId; private static long datacenterId; private static long sequence = 0L ; private static long lastTimestamp = -1L ; public SnowflakeUtil (long workerId, long datacenterId) { if (workerId > MAX_WORKER_ID || workerId < 0 ) { throw new IllegalArgumentException (String.format("worker Id can't be greater than %d or less than 0" , MAX_WORKER_ID)); } if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0 ) { throw new IllegalArgumentException (String.format("datacenter Id can't be greater than %d or less than 0" , MAX_DATACENTER_ID)); } SnowflakeUtil.workerId = workerId; SnowflakeUtil.datacenterId = datacenterId; } public static 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 ) & SEQUENCE_MASK; if (sequence == 0 ) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L ; } lastTimestamp = timestamp; return ((timestamp - START_STAMP) << TIMESTAMP_LEFT_SHIFT) | (datacenterId << DATACENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence; } protected static long tilNextMillis (long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } protected static long timeGen () { return System.currentTimeMillis(); } }
调用静态方法 nextId()
即可获得。
使用 IdWorker 类生成 IdWorker 类在包 com.baomidou.mybatisplus.core.toolkit
中,项目中需要增加 mybatis-plus 依赖项,直接使用 IdWorker.getId()
方法即可获得。
通过 @TableId 注释生成 在使用 @TableId 设置主键映射时:
value 映射主键字段的名字
type 设置主键类型、主键的生成策略
值
描述
AUTO
数据库自增
NONE
MP set主键,雪花算法实现
INPUT
需要手动赋值
ASSIGN_ID
MP分配ID,Long、Integer、String
ASSIGN_UUID
分配UUID,String
可设定 type 为 ASSIGN_ID
自动生成雪花 id。
注意 mybatis-plus 在没有设置机器号的情况下,会通过当前物理网卡地址和 jvm 的进程 id 自动生成机器号,以保证在一个集群中生成的 id 唯一性。但仍有可能会出现自动生成的机器号相同的情况,此时为了保证唯一性,需要手动修改机器号。
参考文章 CSDN - MySQL 数据表主键设计,选择自增 id 还是 UUID 还是雪花 id?
CSDN - MySQL主键:自增ID与UUID的选择
CSDN - 深度思考:雪花算法snowflake分布式id生成原理详解
CSDN - mybatis的分布式主键冲突