在单机环境中,遇到生成不重复Id的问题时,我们一般使用数据库自增主键的形式或者UUID的形式,在分布式系统中,我们使用的不止是一台数据库,使用UUID又会遇到内存消耗过大的情况,并且uuid是没有顺序的,导致B+ tree做索引时有太多的随机操作,性能下降。为了解决这个问题,单独抽出ID生成器模块进行不重复的递增ID生成。

要求

  1. 全局唯一
  2. 基本有序
  3. 高性能
  4. 高可用
  5. 可灵活配置(多种接入方案)

思路

该ID二进制总共有64位,具体如下:

版本 类型 生成方式 秒级时间 序列号 机器ID
63 62 60-61 30-59 10-29 0-10

系统采用多模块的结构,分为:

  1. intf层:接口模块
  2. service层:提供服务的模块 (引入intf依赖)
  3. rest层: 对外提供Rest接口 (引入service依赖)

系统结构为:

各个部分的描述:

  1. Id 类为组成ID的各个字段的结构

  2. IdService接口提供了一个生成号码的方法

  3. AbstractIdService是一个抽象类,实现了genId的方法,组装类型,机器ID等六个子字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    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);
  4. 子类的初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class IdServiceImpl extends AbstractIdService {
    //这个类用来生成序列号
    IdPopulator idPopulator;
    public IdServiceImpl() {
    //父类初始化,设置模式为 秒级 生成,并且设置各个字段所占的字节数
    super.init();
    initPopulator();
    }

    private void initPopulator() {
    idPopulator=new SyncIdPopulator();
    }

    @Override
    protected void populateId(Id id) {
    idPopulator.populateId(id, this.idMate);
    }
    }
  1. 父类的初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public 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);
    }
    ...
    }
  2. 具体生成的时间和序列号设计,逻辑如下,通过锁来控制并发问题,这个地方的逻辑可以通过Lock或者自旋锁来进行设计,同一时间里面的序列号地递增,过了当前时间后的时间改变,同时序列号置为0:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class SyncIdPopulator implements IdPopulator {
    //序列号
    private long sequence=0;
    //时间戳
    private long lastTimestamp=-1;
    @Override
    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);
    }
    }
  3. 额外的时间类(SimpleTimer):时间类用于生成时间的long类型的数值

  4. Abstract类中的拼接,按照更合理的做法,应该是通过IdType类型作判断,然后由工厂类生成指定的拼接方法,这里只是做一个示范:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    protected 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;
    }
  5. 测试:

    1
    2
    3
    4
    5
    6
    7
    public class NtTest {
    @Test
    public void test1(){
    IdServiceImpl idService=new IdServiceImpl();
    System.out.println(idService.genId());
    }
    }

其他

  1. 时间的生成与压缩
    1. 如果并发太大,并且超过了同一秒中的所有的最大序列值,那么可以让其自旋等待下一秒
    2. 压缩:通过ID中类型来确定时间单位,设置一个基准值,用当前值减去基准值,除以一个位数,进行压缩
  2. 机器Id的生成
    1. 通过配置,配置每一台机器上不同的机器ID
    2. 通过IP来分配一个不同的Id
    3. 通过数据库来分配
    4. 通过zookeeper来分配一个