数据库
数据库主键自增
使用 replace into 命令来插入数据,通过 MySQL 的自增主键来产生唯一 ID。
- 优点:实现简单,ID 有序递增,存储空间消耗小。
- 缺点:并发量小,每次获取 ID 都要访问数据库导致数据库的压力很大。
数据库号段模式
可以每次从数据库申请一个号段,也就是一个 ID 的范围,加载到内存中,需要用到的时候,直接从内存里通过自增的方式来生成 ID。一个号段的 ID 用完后可以再次向数据库中申请。
NoSQL
Redis
可以通过 Redis 的 incr 命令来实现 ID 的原子顺序递增。
MongoDB
使用 MongoDB 的 ObjectId 来生成分布式 ID:
4位时间戳、3位机器 ID、2位机器进程 ID、3位自增值
算法
UUID
UUID 是 Universally Unique Identifier(通用唯一标识符)的缩写。UUID 包含32个16进制的数字。
可以直接使用 Java 的 UUID.randomUUID() 方法直接生成。
Snowflake(雪花算法)
传统的雪花算法是由64bit的二进制数字组成。
- sign(1bit):符号位(标识正负),始终为 0,代表生成的 ID 为正数。
- timestamp (41 bits):一共 41 位,表示时间戳,单位是毫秒,可以支撑 约 69 年。
- datacenter id + worker id (10 bits):一般前 5 位表示机房 ID,后 5 位表示机器 ID。这样就可以区分不同集群/机房的节点。
- sequence (12 bits):一共 12 位,表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数,最多可以生成 4096 个 唯一 ID。
但是雪花算法有个很严重的问题,会产生重复 ID。雪花算法 ID 的生成依赖时间,在获取当前时间的时候,服务器上的时间可能会突然倒退到之前的时间,也叫做时间回拨,进而导致重复的 ID。
Seata 改良的雪花算法
标准版雪花算法的缺点
时钟敏感
因为 ID 的生成是和当前操作系统的时间绑定的,如果操作系统发生了时钟回拨现象,生成的 ID 就会重复。
突发性能有上限
解决方案
为了解决时钟敏感问题,解除与操作系统时间戳的时刻绑定,生成器只在初始化时获取系统当前的时间戳,作为初始时间戳, 但之后就不再与系统时间戳保持同步了。它之后的递增,只由序列号的递增来驱动。
为了方便这种溢出进位,调整64位 ID 的分配策略:
- sign(1bit):符号位始终为 0。
- datacenter id + worker id (10 bits):节点 ID。
- timestamp (41 bits):一共 41 位,表示时间戳。
- sequence (12 bits):一共 12 位,表示序列号。
这样时间戳和序列号在内存上是连在一块的,在实现上就很容易用一个 AtomicLong 来同时保存它俩,最高11位可以在初始化时就确定好,之后不再变化。
这样我们可以发现:
-
生成器不再有 4096/ms 的突发性能限制了。如果某个时间戳的序列号空间耗尽,它会直接推进到下一个时间戳, “借用”下一个时间戳的序列号空间。
-
生成器弱依赖于操作系统时钟。在运行期间,生成器不受时钟回拨的影响,因为生成器仅在启动时获取了一遍系统时钟,之后两者不再保持同步。唯一可能产生重复 ID 的只有在重启时的大幅度时钟回拨,比如人为刻意回拨或者修改操作系统时区,如北京时间改为伦敦时间。机器时钟漂移基本是毫秒级的,不会有这么大的幅度。
-
持续不断的”超前消费”实际上并不会使生成器内的时间戳大大超前于系统的时间戳, 从而在重启时造成 ID 重复。因为要达到这种效果,意味该生成器接收的 QPS 得持续稳定在 400w/s 之上。流量太高了。
节点 ID 优先从本机网卡的 MAC 地址截取低10位,如果本机未配置有效的网卡,则在[0, 1023]中随机挑一个作为节点 ID。
PS:参考文章
I am a rot
233
跪了
沙发