在单机环境中,遇到生成不重复Id的问题时,我们一般使用数据库自增主键的形式或者UUID的形式,在分布式系统中,我们使用的不止是一台数据库,使用UUID又会遇到内存消耗过大的情况,并且uuid是没有顺序的,导致B+ tree做索引时有太多的随机操作,性能下降。为了解决这个问题,单独抽出ID生成器模块进行不重复的递增ID生成。
要求
- 全局唯一
- 基本有序
- 高性能
- 高可用
- 可灵活配置(多种接入方案)
思路
该ID二进制总共有64位,具体如下:
版本 | 类型 | 生成方式 | 秒级时间 | 序列号 | 机器ID |
---|---|---|---|---|---|
63 | 62 | 60-61 | 30-59 | 10-29 | 0-10 |
系统采用多模块的结构,分为:
- intf层:接口模块
- service层:提供服务的模块 (引入intf依赖)
- rest层: 对外提供Rest接口 (引入service依赖)
系统结构为:

各个部分的描述:
Id 类为组成ID的各个字段的结构
IdService接口提供了一个生成号码的方法
AbstractIdService是一个抽象类,实现了genId的方法,组装类型,机器ID等六个子字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public long genId(){
Id id = new Id();
id.setMachine(machineId);
id.setGenMethod(genMethod);
id.setType(idType.value());
id.setVersion(version);
//由子类生成Id的序列号和验证码
populateId(id);
//转化id
long ret = convert(id, this.idMate);
return ret;
}
protected abstract void populateId(Id id);子类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class IdServiceImpl extends AbstractIdService {
//这个类用来生成序列号
IdPopulator idPopulator;
public IdServiceImpl() {
//父类初始化,设置模式为 秒级 生成,并且设置各个字段所占的字节数
super.init();
initPopulator();
}
private void initPopulator() {
idPopulator=new SyncIdPopulator();
}
protected void populateId(Id id) {
idPopulator.populateId(id, this.idMate);
}
}
父类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public abstract class AbstractIdService implements IdService {
protected long machineId=1;
protected long genMethod=0;
protected long version=0;
protected IdType idType;
protected IdMate idMate;
public void init(){
//设置为类型为秒级
idType=IdType.SECONDS;
//设置各个字段所占的位数
idMate= new IdMate((byte) 10, (byte) 20, (byte) 30, (byte) 2, (byte) 1, (byte) 1);
}
...
}具体生成的时间和序列号设计,逻辑如下,通过锁来控制并发问题,这个地方的逻辑可以通过Lock或者自旋锁来进行设计,同一时间里面的序列号地递增,过了当前时间后的时间改变,同时序列号置为0:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class SyncIdPopulator implements IdPopulator {
//序列号
private long sequence=0;
//时间戳
private long lastTimestamp=-1;
public synchronized void populateId(Id id, IdMate idMate) {
Timer timer=new SimpleTimer();
long timestamp= timer.genTime();
if(timestamp==lastTimestamp){
sequence++;
}else {
lastTimestamp=timestamp;
sequence=0;
}
id.setSeq(sequence);
id.setTime(timestamp);
}
}额外的时间类(SimpleTimer):时间类用于生成时间的long类型的数值
Abstract类中的拼接,按照更合理的做法,应该是通过IdType类型作判断,然后由工厂类生成指定的拼接方法,这里只是做一个示范:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17protected long doConvert(Id id, IdMate idMeta) {
long ret = 0;
ret |= id.getMachine();
ret |= id.getSeq() << idMeta.getSeqBitsStartPos();
ret |= id.getTime() << idMeta.getTimeBitsStartPos();
ret |= id.getGenMethod() << idMeta.getGenMethodBitsStartPos();
ret |= id.getType() << idMeta.getTypeBitsStartPos();
ret |= id.getVersion() << idMeta.getVersionBitsStartPos();
return ret;
}测试:
1
2
3
4
5
6
7public class NtTest {
public void test1(){
IdServiceImpl idService=new IdServiceImpl();
System.out.println(idService.genId());
}
}
其他
- 时间的生成与压缩
- 如果并发太大,并且超过了同一秒中的所有的最大序列值,那么可以让其自旋等待下一秒
- 压缩:通过ID中类型来确定时间单位,设置一个基准值,用当前值减去基准值,除以一个位数,进行压缩
- 机器Id的生成
- 通过配置,配置每一台机器上不同的机器ID
- 通过IP来分配一个不同的Id
- 通过数据库来分配
- 通过zookeeper来分配一个