生成分布式ID的四种常见方案对比
文章目录
简介:
这篇博客从零开始,介绍了分布式id的使用场景和重要性,并且列举了目前最常见的分布式id生成方法。包括 UUID、单独自增主键、雪花算法、Redis的incr命令等。
分布式id:分布式集群环境下的全局唯一ID
为什么需要分布式ID?
举例:在Mysql分表的情况下,如果每个表都使用自增主键,那么后期数据将无法根据id区分唯一的数据项。主键会存在冲突(因为都是按顺序自增)
一、分布式ID方案之 UUID(可以用)
UUID:全称是universally Unique Identifier (通用唯一识别码),生成uuid重复的问题几乎可以忽略不急,概率非常低。JAVA语言中得到uuid可以使用java.util包提供的方法 java.util.UUID.randomUUID()
缺点: 内容长且随机,如果uuid用作索引对查询的性能提升很小
使用:使用简单,只要能忍受带来的缺点就可以使用,并且目前也有很多公司在用
二、分布式ID方案之 独立数据库自增ID(不推荐)
自增方案
- 独立数据库,创建一个不保存业务数据的专门用来生成主键的表
- 每次插入数据前,向主键表插入一条数据,利用其自增的主键当做业务表的数据主键
优缺点分析
- 优点:解决了业务表分表后使用自增id重复的问题,并且可以生成整形等利用索引的格式
- 缺点:可靠性和性能都受到影响,生成id必须要链接数据库增加了性能消耗。并且给系统增加了不稳定因素,Mysql挂了就没法儿用了。
使用:很少使用,不是很推荐
三、分布式ID方案之 雪花算法Snowflake(可以用)
3.1 Snowflake介绍
什么是snowflake: 是推特推出的一个用于生成分布式id的策略,基于这个算法生成的id是一个long型。在java中long型是一个8字节,算下来有64bit。
国内的互联⽹公司也基于上述的⽅案封装了⼀些分布式ID⽣成器,⽐如滴滴的tinyid(基于数据库实现)、百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和SnowFlake)
snowflake原理
- 符号位: 固定为0,二进制表示最高位是符号位,0代表正数,1代表负数
- 时间戳: 41个二进制数用来记录时间截,表示某一个毫秒 (毫秒级)
- 机器id:代表当前算法运行机器的id
- 序列号:12位,用来记录某个机器同一个毫秒内产生的不同序列号,代表同一个机器同一个毫秒可以产生的ID序号。
优点:
- 由于有机器号,可以保证在分布式环境下的id唯一性
- 序列号的机制,保证了其在单机上生成的id的有序性
缺点:
- 依赖于时间一致性,如果出现时间回拨的情况,就可能出现问题
- 在单机上递增。在分布式多态机器上,只是大致递增趋势不会严格递增。
3.2 JAVA的snowflake源码
/**
* 官方推出,Scala编程语言来实现的
* Java前辈用Java语言实现了雪花算法
*/
public class IdWorker{
//下面两个每个5位,加起来就是10位的工作机器id
private long workerId; //工作id
private long datacenterId; //数据id
//12位的序列号
private long sequence;
public IdWorker(long workerId, long datacenterId, long sequence){
// sanity check for workerId
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));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
//初始时间戳
private long twepoch = 1288834974657L;
//长度为5位
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
//最大值
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
//序列号id长度
private long sequenceBits = 12L;
//序列号最大值
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//工作id需要左移的位数,12位
private long workerIdShift = sequenceBits;
//数据id需要左移位数 12+5=17位
private long datacenterIdShift = sequenceBits + workerIdBits;
//时间戳需要左移位数 12+5+5=22位
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//上次时间戳,初始值为负数
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId(){
return datacenterId;
}
public long getTimestamp(){
return System.currentTimeMillis();
}
//下一个ID生成算法
public synchronized long nextId() {
long timestamp = timeGen();
//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//获取当前时间戳如果等于上次时间戳
//说明:还处在同一毫秒内,则在序列号加1;否则序列号赋值为0,从0开始。
if (lastTimestamp == timestamp) { // 0 - 4095
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//将上次时间戳值刷新
lastTimestamp = timestamp;
/**
* 返回结果:
* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
* (workerId << workerIdShift) 表示将工作id左移相应位数
* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
//获取时间戳,并与上次时间戳比较
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取系统时间戳
private long timeGen(){
return System.currentTimeMillis();
}
public static void main(String[] args) {
IdWorker worker = new IdWorker(21,10,0);
for (int i = 0; i < 100; i++) {
System.out.println(worker.nextId());
}
}
}
四、分布式id之 Redis的Incr(推荐)
Incr命令,会将存储在key上的数字加1。如果该键不存在或包含错误类型的值,请先将该键值设置为“0”,再进行加1操作。借用此特性,我们可以天然获得唯一且递增的分布式id。
假设key为"id",则redis中的数据变化如下
- 执行前:<>
- 第一次执行Incr 创建key并赋值 0 <“id”,“0”>,再执行incr 结果为<“id”,“1”>
- 第二次执行incr 结果为 <“id”,“2”>
- …
java使用案例
public static void main(String[] args) {
Jedis jedis = new Jedis("111.229.248.243", 6379);
Long id = jedis.incr("id");//<id,0>
System.out.println(id);
}
五、总结
分布式id的重要性在如今的分布式系统环境中的是不言而喻的。上文中提到的UUID、数据库自增主键、雪花算法、和利用redis的incr命令实现分布式id等方法,都能达到分布式id的效果。
1. 对于业务场景较小且后期拓展可能性较小的项目,使用UUID最为简单。
2. 数据库自增主键和Redis的incr命令的话,都是利用第三方生成唯一自增值的方式来实现的分布式id,但是使用数据库自增主键的话,需要和数据库绑定,需要额外的维护且受数据量大小的影响,使用redis则简单许多比较推荐。
3. 雪花算法的话,像是在大量服务器的场景下使用会比较省事,但是也需要保证时间同步的问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?