雪花算法简介
SnowFlake 中文意思为雪花,故称为雪花算法。最早是 Twitter 公司在其内部用于分布式环境下生成唯一 ID。
雪花算法有以下几个优点:
- 高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
- 基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
- 不依赖第三方库或者中间件。
- 算法简单,在内存中进行,效率高。
雪花算法有如下缺点:
- 依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。
- 需要配置机器ID和服务器ID
[参考来源](SnowFlake 雪花算法详解与实现 - 掘金 (juejin.cn))
容器化部署雪花算法遇到的问题
-
容器化无状态部署机器ID不可获取
以前项目使用物理机器部署时,我们可以根据机器的IP分配对就的机器id,可是现在都是使用容器化部署,一般都是部署成无状态模式,无法获取workId;
-
一个容器一般只部署一个服务,所有服务id可以不需要了。
解决思路
- 将机器id和服务id合并
- 项目启用时通过redis获取一个workId
RID的诞生
基本思路
- 为每个微服务配置一个rid.redisKey,当作该服务在redis中的唯一标识服务
- 在项目启用时,将rid.redisKey自增,获取到一个workId
- 将wordId与maxWorkerId取余运算,得到单个服务的唯一workId
核心代码
/**
* 基本Redis生成ID
*/
@Component
public class ID {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static Long REDIS_ID;
/**
* redis KEY 不同项目需要修改
*/
@Value("${rid.redisKey}")
private String ID_REDIS_KEY = "RID";
/**
* 起始时间戳
*/
@Value("${rid.startStamp}")
private Long startStamp = 1577808000000L;
/**
* 机器id所占的位数 最多机器节点2^5=32个
*/
@Value("${rid.workerIdBits}")
private final long workerIdBits = 5L;
/**
* 序列号所占的位数 决定单个容器每毫秒生成速度,默认每毫秒生成 2^7=128
*/
@Value("${rid.sequenceBits}")
private final long sequenceBits = 7L;
/**
* 时间戳位数 从startStamp开始可以 2^41/(1000606024365)=69,大概可以使用69年。
*/
private final long timeStampBits = 41L;
private Long workerId;
@PostConstruct
void init() {
REDIS_ID = stringRedisTemplate.opsForValue().increment(ID_REDIS_KEY) % maxWorkerId;
}
/**
* 时间戳最大值
*/
private final long maxTimeStamp = ~(-1L << timeStampBits);
/**
* 机器id的最大值
*/
private final long maxWorkerId = ~(-1L << workerIdBits);
/**
* 序列号的最大值
*/
private final long maxSequence = ~(-1L << sequenceBits);
private long sequence = 0L;
private long lastTimeStamp = -1L;
public synchronized long id() {
long currentTimeStamp = timeGen();
if (currentTimeStamp < lastTimeStamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimeStamp - currentTimeStamp));
}
if (lastTimeStamp == currentTimeStamp) {
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
currentTimeStamp = tilNextMillis(lastTimeStamp);
}
} else {
sequence = 0L;
}
lastTimeStamp = currentTimeStamp;
return ((currentTimeStamp - startStamp) & maxTimeStamp) << (sequenceBits + workerIdBits) | (workerId << sequenceBits) | sequence;
}
private long tilNextMillis(long lastTimeStamp) {
long timeStamp = timeGen();
while (timeStamp <= lastTimeStamp) {
timeStamp = timeGen();
}
return timeStamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
private static class SingletonHolder {
private static ID ID;
static {
ID = new ID(REDIS_ID);
}
}
public static ID getInstance() {
return SingletonHolder.ID;
}
private ID() {
}
private ID(long workerId) {
this.workerId = workerId;
}
}
@Component
public class RID {
public static Long generateId() {
return ID.getInstance().id();
}
}
测试代码
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
public class SpringBootApplicationTests {
@Test
public void testId() {
Set<Long> ids = new HashSet<>();
int size = 1000000;
long startTime=System.currentTimeMillis();
for (int i = 0; i < size; i++) {
ids.add(RID.generateId());
}
System.out.println(String.format("generate %d ids spend %s ms",size,System.currentTimeMillis()-startTime));
Assert.assertEquals(size, ids.size());
}
}
优点
- 无需配置机器ID,服务ID,可以用微服务的applicationId当作rid.redisKey,可无状态化部署
- 单个微服务容器最大节点数量及生成速度可配置
- ID大部分情况是自增了
存在问题
- ID只能做到单个容器唯一,不可做到全局唯一
- 2^workerIdBits> 2倍容器数据,容器节点重启会生成新的容器,然后替换原来老容器
- 集群中如果有容器一直不重启,后面重启容器可能会分配到相当的workId导致ID重复
- 生成的ID位数不固定 当前时间-startStamp 位数会随着时间增加,数据库不要用varchar类型
- 服务器时间回拨,可能导致ID重复
- ID不是严格全局自增,同一毫秒内rid.redisKey达到maxWorkerId后从0开始,可能导致生成ID比其它服务生成的小
最后
- 这个ID生成器满足我们自己的需求,能不能满足你们的需求,自行评估
- 个人能力有限,应该还是很多没考虑到的
- 源码地址 一种适合容器化部署的雪花算法ID生成器