分布式唯一id生成

在实际应用中,对于分布式系统,我们希望能有一个ID生成器用来生成全局的、唯一的ID,比如用户id,订单号,支付单号等等。

目前,存在的主要有以下几种。

1、UUID

这个应该读很熟悉,通用唯一识别码,它是由32位的16进制数组成的。

类似:00112233-4455-6677-8899-aabbccddeeff。

其生成规则有很多的版本,有根据网卡或时间生成的,有随机生成的,有基于时间生成的等等。具体可见: UUID维基百科

但其并不适合做为我们实际系统中的ID生成器,因为其是随机的,无序的,在实际中我们系统它是有序的,此外无序的索引在Mysql中查询数据也是有性能的影响。此外,UUID标识位较多,较占空间。

2、数据库主键自增

使用Mysql的主键当作ID生成器,不同的系统可以统一通过DB代理访问数据库,并获取生成的主键ID。

这种方式是相对比较简单的一种做法,不需要额外做什么操作,只需要部署好DB。但其本身也存在缺点,因为该方式是强依赖DB的,假如DB挂了,那服务完全不可用。且ID生产性能受限于Mysql性能。此外,如果后续Mysql分库分表的话,很难保证ID不出现重复。

3、Redis,zk

其实说实话,基本没有用的,虽然两个都能实现。然而Redis非常可能导致数据丢失,从而出现重复。zk的话本身集群的性能一般。

4、SnowFlake算法

该算法是Twitter开源的一个算法,生成的ID是由63位数字组成的long型整数,41位的时间戳+5位的数据中心id+5位机器id+12位递增序列号。

第1位是符号位,0是正数,1是负数。

41位时间戳是毫秒时间戳,不过这个时间戳并不是绝对时间戳,而是一个差值,差值是当前时间戳减去起始时间戳,而起始时间戳的值取决于自己的选择。时间戳最多可以用69年。

中间的10位是机器ID,由5为数据中心ID和5为机器ID组成,也就是总共可部署1024个机器。

最后的12bit是循环位,用来同一毫秒内生成不同的id,12位最多可以生成4095个,因此同一毫秒内,允许生成4095个ID。

该算法最大的缺点就是性能超好,没有其他的依赖,全局唯一。如果部署1024个机器的话,理论上美妙可产生40多亿个id。

但该算法有一个众所周知的缺点就是时钟回拨的问题,即因为机器的原因,时钟回拨了,可能导致当前时间戳还要小于之前的时间戳,这样就出现ID重复。

针对这个问题,目前有很多的解决方案。

方案1:每台都维护一个上个时间戳 lastTimestamp,每次都会将当前时间戳和上一个时间戳进行比较。如果查值较小(有多小完全取决于实际业务需求,可以5ms,可以10ms),可以等待,一直到时间超过lastTimestamp;如果差值较大,无法一直阻塞,可以直接抛出异常,然后将时钟回滚。或者增加在后面再增加扩展位(但我觉得这种方式不妥,因为扩展位的位数也是有限的,不是解决问题的优先方案);

方案2:百度提了一个UIdGenerator,源码: UIDGenerator .

其基本思想是不采用SnowFlake算法中传统的取当前时间戳,而是通过AtomicLong类型,采用逐步+1的方式生成时间戳,这里只需要维护一个初始时间戳即可。这样不需要再依赖服务器的时间,不会出现回拨问题。

当然这样做的弊端就是,如果你希望通过序列号来获取时间的话,这样做是不可取的,因为通过这个方法获取的不一定就是那时实际的时间戳。

目前这个项目,最后一次维护还是3年前。

5、美团Leaf

其主要有两个方案:Leaf-segment方案和Leaf-snowflake方案。

Leaf-segment方案就是基于数据库自增ID来实现的,它在数据库的性能上的提升做了一定的改进,即它一利用代理去一次生成一段的ID(segment),然后存储下来,等用完了再去请求数据库。这样做的一个明显有点就是没必要每次生成ID都需要请求一次数据库,减少IO,提升了性能。此外,在此基础上,美团又提出了buffer的优化,即当已获取的一段ID号用了一定数量后,且未完全耗尽时,会异步再去请求数据库并存储下来。这对客户端来讲时无感知的,因此性能又得到了极大的提升。

Leaf-snowflake方案是一种类似snowflake的一种实现方式。它主要是采用了ZK去管理配置机器节点。

但说实话,我不知道这个方案有什么实际的意义,我是觉得用了ZK反倒影响性能。

关于Leaf更详细的介绍可见: Leaf——美团点评分布式ID生成系统

下面是用JAVA实现的SnowFlake算法,改进之处就是对于时钟回拨的处理,参考了美团。

public class OrderIdGeneratorServiceImpl implements OrderIdGeneratorService {


    private static final Logger logger = LoggerFactory.getLogger(OrderIdGeneratorServiceImpl.class);

    /**
     * work开始id
     */
    public static final AtomicLong START_WORK_ID = new AtomicLong(0);


    public static final Long START_TIMESTAMP = 1580580122000L;
    /**
     * ID 首位
     */
    private static final Short ID_HEADER = 1;


    private static final Long WorkIDBits = 8L;

    private static final Long MAX_WORK_ID =  ~(-1L << WorkIDBits); //最大的work id

    private static final Long CycleIdBits = 12L;

    private static final Long MAX_CYCLE_ID = ~(-1L << CycleIdBits);

    private static final Long WorkIdShifts = CycleIdBits;
    private static final Long TimeStampShifts = CycleIdBits + WorkIDBits;


    private Long workerId;
    private Long sequenceId = 0L;
    private Long lastTimeStamp = -1L;


    @Override
    public synchronized Long getOrderId() throws OrderIdGenerationException {
        Long timestamp = this.getCurTimeStamp();
        if(timestamp < this.lastTimeStamp) {
            Long delta = this.lastTimeStamp - timestamp;
            Long MAX_DElTA = 5L;
            if(delta <= MAX_DElTA){
               try{
                   //设定一个休眠时间
                   wait(delta << 1);
                   timestamp = this.getCurTimeStamp();
                   if(timestamp < lastTimeStamp){
                       throw new OrderIdGenerationException("时钟已经回拨,请检查机器时钟");
                   }
               }catch (Exception e){
                   throw new OrderIdGenerationException(e.getMessage());
               }
            }else {
                throw new OrderIdGenerationException("时钟已经回拨,请检查机器时钟");
            }
        }

        if(timestamp.equals(this.lastTimeStamp)){
            sequenceId = (sequenceId + 1 ) & MAX_CYCLE_ID;
            if(sequenceId == 0){
                timestamp = tillNextMill(lastTimeStamp);
            }
        }else {
            //如果不是在同一毫秒,直接返回0
            sequenceId = 0L;
        }
        lastTimeStamp = timestamp;
        workerId = this.getCurWorkId();
        logger.info("time:{},work:{},sequence:{}",timestamp,workerId,sequenceId);
        return (timestamp - START_TIMESTAMP) << TimeStampShifts |
                workerId << WorkIdShifts |
                sequenceId;
    }


    private Long getCurTimeStamp(){
        return  System.currentTimeMillis();
    }

    private Long tillNextMill(Long lastTimeStamp){
        Long timestamp = this.getCurTimeStamp();
        while(timestamp <= lastTimeStamp){
            timestamp = this.getCurTimeStamp();
        }
        return  timestamp;
    }

    /**
     *
     * 工作节点,要使用 ZK来管理,保证该机器异常时,要摘除这个id
     * @return
     */
    private Long getCurWorkId() throws OrderIdGenerationException {
        //TODO 通过zookeeper获取workid,通过ZK感知其可用性,目前暂时使用原子变量替换,
        this.workerId = START_WORK_ID.incrementAndGet() & MAX_WORK_ID;
        logger.info("work id:{}",this.workerId);
        if (this.workerId > MAX_WORK_ID){
            throw new OrderIdGenerationException(String.format("work ID 超过最大值:%d",MAX_WORK_ID));
        }
        if (this.workerId < 0L ){
            throw new OrderIdGenerationException("work ID 不可为负数");
        }
        //TODO 通过zookeeper获取workid
        return this.workerId;
    }

}

上述的workId应该采用ZK管理比较合适,我上面就是随意瞎写的,不规范,因为workerid是和机器有关的,每台机器是一样的。

例子:

public class SnowFlake {
    protected long workerId;
    protected long datacenterId;
    protected long sequence;
    private static final long START_STMP = 1617206400000L;
    private long workerIdBits = 8L;
    private long datacenterIdBits = 2L;
    protected long maxWorkerId;
    protected long maxDatacenterId;
    private long sequenceBits;
    private long sequenceMask;
    private long workerIdShift;
    private long datacenterIdShift;
    private long timestampLeftShift;
    protected long lastTimestamp;
    private final byte[] lock;
    private static AtomicLong STORE_CNT = new AtomicLong();

    public SnowFlake() {
        this.maxWorkerId = ~(-1L << (int)this.workerIdBits);
        this.maxDatacenterId = ~(-1L << (int)this.datacenterIdBits);
        this.sequenceBits = 12L;
        this.sequenceMask = ~(-1L << (int)this.sequenceBits);
        this.workerIdShift = this.sequenceBits;
        this.datacenterIdShift = this.sequenceBits + this.workerIdBits;
        this.timestampLeftShift = this.sequenceBits + this.workerIdBits + this.datacenterIdBits;
        this.lastTimestamp = -1L;
        this.lock = new byte[0];
        this.datacenterId = this.makeDatacenterId(this.maxDatacenterId);
        this.workerId = this.makeWorkerId(this.datacenterId, this.maxWorkerId);
        this.sequence = 0L;
    }

    public SnowFlake(long datacenterId, long workerId, long sequence) {
        this.maxWorkerId = ~(-1L << (int)this.workerIdBits);
        this.maxDatacenterId = ~(-1L << (int)this.datacenterIdBits);
        this.sequenceBits = 12L;
        this.sequenceMask = ~(-1L << (int)this.sequenceBits);
        this.workerIdShift = this.sequenceBits;
        this.datacenterIdShift = this.sequenceBits + this.workerIdBits;
        this.timestampLeftShift = this.sequenceBits + this.workerIdBits + this.datacenterIdBits;
        this.lastTimestamp = -1L;
        this.lock = new byte[0];
        if (workerId <= this.maxWorkerId && workerId >= 0L) {
            if (datacenterId <= this.maxDatacenterId && datacenterId >= 0L) {
                this.workerId = workerId;
                this.datacenterId = datacenterId;
                this.sequence = sequence;
            } else {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0\r\n", this.maxDatacenterId));
            }
        } else {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0\r\n", this.maxWorkerId));
        }
    }

    public long nextId() {
        synchronized(this.lock) {
            long timestamp = this.timeGen();
            if (timestamp < this.lastTimestamp) {
                System.err.printf("clock is moving backwards.  Rejecting requests until %d.", this.lastTimestamp);
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", this.lastTimestamp - timestamp));
            } else {
                if (this.lastTimestamp == timestamp) {
                    this.sequence = this.sequence + 1L & this.sequenceMask;
                    if (this.sequence == 0L) {
                        timestamp = this.getNextMillis(this.lastTimestamp);
                    }
                } else {
                    this.sequence = 0L;
                }

                this.updateLastTimestamp(timestamp);
                return timestamp - 1617206400000L << (int)this.timestampLeftShift | this.datacenterId << (int)this.datacenterIdShift | this.workerId << (int)this.workerIdShift | this.sequence;
            }
        }
    }

    protected long makeWorkerId(long datacenterId, long maxWorkerId) {
        long workId = this.randomLong(this.maxDatacenterId);

        try {
            StringBuffer sb = new StringBuffer();
            sb.append(datacenterId);
            String localIp = Utils.getIp();
            sb.append(localIp);
            String name = ManagementFactory.getRuntimeMXBean().getName();
            if (null != name && !name.isEmpty()) {
                sb.append(name.split("@")[0]);
            }

            sb.append(this.randomLong(this.maxDatacenterId));
            workId = (long)(sb.toString().hashCode() & '\uffff') % (maxWorkerId + 1L);
        } catch (Exception var10) {
            System.out.println("makeWorkerId Exception:" + var10.getMessage());
        }

        return workId;
    }

    protected long recoverLastTimestamp() {
        return -1L;
    }

    private void updateLastTimestamp(long newTime) {
        this.lastTimestamp = newTime;
        this.storeLastTimestamp(this.lastTimestamp);
    }

    protected void storeLastTimestamp(long lastTimestamp) {
        STORE_CNT.addAndGet(1L);
    }

    private long randomLong(long maxId) {
        return ThreadLocalRandom.current().nextLong(0L, maxId);
    }

    protected long makeDatacenterId(long maxDatacenterId) {
        long datacenterId = 0L;
        return datacenterId;
    }

    public long getWorkerId() {
        return this.workerId;
    }

    public long getDatacenterId() {
        return this.datacenterId;
    }

    private long getNextMillis(long lastTimestamp) {
        long timestamp = this.timeGen();

        do {
            if (timestamp > lastTimestamp) {
                return timestamp;
            }

            timestamp = this.timeGen();
        } while(lastTimestamp - timestamp <= 3000L);

        throw new RuntimeException(String.format("clock is moving backwards.  Rejecting requests until %s.", lastTimestamp));
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }
}

Over~

--------EOF---------
微信分享/微信扫码阅读