背景
在id生成器中,我们自己手写一个自增的id生成器很简单,也很好用。但这只是单机中的id生成器,当我们在集群中使用时,一个集群就会有一个id生成器实例,就意味着每一个集群的id都会从0开始,最后就会导致id出现重复的情况。这种情况下有没有更好的id生成器呢?
雪花算法(Snowflake)最初是在Twitter的内部产生的,其主要背景是Twitter在高并发环境下对唯一ID生成的需求。由于Twitter面临着大量的用户和数据交互,因此需要一种高效且能够生成全局唯一ID的算法来支持其分布式系统的运行。雪花算法就是在这样的背景下应运而生,并且由于其高效、可靠的特点,逐渐被广泛使用。(世界上没有一片雪花是相同的)
雪花算法
用一个接地气的说法来解释一下雪花算法。(此解释不一定是准确算法,但大致意思正确,为了更好地理解)
我们可以对每个机房进行id编号,在这基础上再对每个机房中的服务进行编号,这样生成的每一个id就不会出现重复的情况,就实现了全局id的唯一。
雪花算法的实现
机房号
机器号
时间戳
序列号(同一个机房同一个机器同一个时间时,想要并发产生大量id就使用到了序列号,就不会出现重复的id)。
我们可以设置机房号位5bit,机器号为5bit,时间戳是64bit以内,根据序列号使用12bit来规定,我们可以将时间戳设置为 42bit。
雪花算法的实现在宏观上来看就是将42位的时间戳左移22位让他占在id的前42位,然后让机房号占用第43到47位,以此类推,最后生成一个唯一的id。
我们先将这些号码位数规定好
java
//起始时间戳
public static final long START_STAMP = DateUtils.get("2024-1-1").getTime();
public static final long ROOM_BIT = 5L;
public static final long MACHINE_BIT = 5L;
public static final long SEQUENCE_BIT = 12L;
然后将他们的最大值设定好来防止他们超过最大值而产生重复值。
java
public static final long ROOM_MAX = ~(-1L << ROOM_BIT);
public static final long MACHINE_MAX = ~(-1L << MACHINE_BIT);
public static final long SEQUENCE_MAX = ~(-1L << SEQUENCE_BIT);
然后设定好他们要左移的位数。
java
public static final long TIME_STAMP_LEFT = ROOM_BIT + MACHINE_BIT + SEQUENCE_BIT;
public static final long ROOM_LEFT = MACHINE_BIT + SEQUENCE_BIT;
public static final long MACHINE_LEFT = SEQUENCE_BIT;
声明好各个id。
java
private long roomId;
private long machineId;
private AtomicLong sequenceId = new AtomicLong(0);
我们现在需要一个构造器来指定机房号和机器号。
java
public IdGenerator(long roomId, long machineId) {
if (roomId > ROOM_MAX || machineId > MACHINE_MAX){
throw new IllegalArgumentException("机房号id或者机器号id不合法");
}
this.roomId = roomId;
this.machineId = machineId;
}
然后是获取id的逻辑,大体思路是获取当前与开始时间戳的差值时间戳,比较一下是否比上一次时间戳小,如果小的话,就抛出时钟回拨异常,然后与上次记录的时间戳进行比较,如果相同就使用原子类不断累加序列号,最后将这些id左移后并进行返回。
java
public long getId(){
long currentTime = System.currentTimeMillis();
long timeStamp = currentTime - START_STAMP;
if (timeStamp < lastTimeStamp){
throw new RuntimeException("出现时钟回拨问题。");
}
if (timeStamp == lastTimeStamp){
sequenceId.incrementAndGet();
if (sequenceId.longValue() >= SEQUENCE_MAX){
timeStamp = getNextTimeStamp();
sequenceId.set(0);
}
} else {
sequenceId.set(0);
}
lastTimeStamp = timeStamp;
long sequence = sequenceId.longValue();
return timeStamp << TIME_STAMP_LEFT | roomId << ROOM_LEFT | machineId << MACHINE_LEFT
| sequence;
}
在序列号累加之中可能会出现累加后大于最大值的情况,这时候我们可以再次获取一次时间戳,开始下一次的时间戳来生成id。
java
private long getNextTimeStamp() {
long timeStamp = System.currentTimeMillis() - START_STAMP;
while (timeStamp == lastTimeStamp){
timeStamp = System.currentTimeMillis() - START_STAMP;
}
return timeStamp;
}
以上就是雪花算法的实现,下面附上全部代码。
java
package com.mhz;
import java.util.concurrent.atomic.AtomicLong;
/**
* 请求id生成器
*/
public class IdGenerator {
//起始时间戳
public static final long START_STAMP = DateUtils.get("2024-1-1").getTime();
public static final long ROOM_BIT = 5L;
public static final long MACHINE_BIT = 5L;
public static final long SEQUENCE_BIT = 12L;
public static final long ROOM_MAX = ~(-1L << ROOM_BIT);
public static final long MACHINE_MAX = ~(-1L << MACHINE_BIT);
public static final long SEQUENCE_MAX = ~(-1L << SEQUENCE_BIT);
public static final long TIME_STAMP_LEFT = ROOM_BIT + MACHINE_BIT + SEQUENCE_BIT;
public static final long ROOM_LEFT = MACHINE_BIT + SEQUENCE_BIT;
public static final long MACHINE_LEFT = SEQUENCE_BIT;
private long roomId;
private long machineId;
private AtomicLong sequenceId = new AtomicLong(0);
private long lastTimeStamp;
public IdGenerator(long roomId, long machineId) {
if (roomId > ROOM_MAX || machineId > MACHINE_MAX){
throw new IllegalArgumentException("机房号id或者机器号id不合法");
}
this.roomId = roomId;
this.machineId = machineId;
}
public long getId(){
long currentTime = System.currentTimeMillis();
long timeStamp = currentTime - START_STAMP;
if (timeStamp < lastTimeStamp){
throw new RuntimeException("出现时钟回拨问题。");
}
if (timeStamp == lastTimeStamp){
sequenceId.incrementAndGet();
if (sequenceId.longValue() >= SEQUENCE_MAX){
timeStamp = getNextTimeStamp();
sequenceId.set(0);
}
} else {
sequenceId.set(0);
}
lastTimeStamp = timeStamp;
long sequence = sequenceId.longValue();
return timeStamp << TIME_STAMP_LEFT | roomId << ROOM_LEFT | machineId << MACHINE_LEFT
| sequence;
}
private long getNextTimeStamp() {
long timeStamp = System.currentTimeMillis() - START_STAMP;
while (timeStamp == lastTimeStamp){
timeStamp = System.currentTimeMillis() - START_STAMP;
}
return timeStamp;
}
public static void main(String[] args) {
IdGenerator idGenerator = new IdGenerator(1,2);
for (int i = 0; i < 1000; i++) {
new Thread(() -> System.out.println(idGenerator.getId())).start();
}
}
}