分布式id生成器优化

已有实现

  • 核心代码

    
    public synchronized long nextId() {
        long timestamp = timeGen();
    
        // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
            lastTimestamp - timestamp));
        }
    
        // 如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            // 毫秒内序列溢出
            if (sequence == 0) {
                // 阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        // 时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }
    
        // 上次生成ID的时间截
        lastTimestamp = timestamp;
    
        // 移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift)
            | (datacenterId << datacenterIdShift)
            | (workerId << workerIdShift)
            | sequence;
    }
    由上述的代码可知,本质上生成一个分布式id主要包括3段:41bit时间戳 + 10bit机器标识(这里的实现是5bit的机器码+5bit数据中心码)+ 12bit的序列号 Image not found
  • 已有实现的问题

    • 没有使用10bit机器标识。目前的机器码和数据中心码都是写死为1,在所有场景下这10bit完全一样,如果在分布式环境下,遇到一些机器时间漂移的情况,虽然趋势shi递增,但不是绝对递增,那么很可能生成重复的id;
    • 没有充分利用序列号。上述代码遇到不是在同一时间毫秒内,则直接写死序列号为0.同样地很大概率会导致最后的序列号都是0,如果在分布式环境,机器时间漂移,依然容易产生重复的id;
      // 时间戳改变,毫秒内序列重置
      else {
         sequence = 0L;
      }
      
    • 对系统时钟回退的误差没有容忍度,而是直接抛出异常

测试

由上述可知,只要时间戳一样,那么生成的id就是一样的,经过测试确实也是如此。

  /**
   * 返回以毫秒为单位的当前时间
   *
   * @return 当前时间(毫秒)
   */
  protected long timeGen() {
      return twepoch + 1;
      // return System.currentTimeMillis();
  }
为了简单,直接修改timeGen函数,让其返回时间一样,则生成的id都一样。

Image not found

优化

  • 生成机器id。通过获取机器MAC地址来生成
    /**
     * 获取机器ID,使用进程ID配合数据中心ID生成<br>
     * 机器依赖于本进程ID或进程名的Hash值。
     *
     * <p>
     * 此算法来自于mybatis-plus#Sequence
     * </p>
     *
     * @param datacenterId 数据中心ID
     * @param maxWorkerId  最大的机器节点ID
     * @return ID
     */
    public static long getWorkerId(long datacenterId, long maxWorkerId) {
        final StringBuilder mpid = new StringBuilder();
        mpid.append(datacenterId);
        try {
            final String processName = ManagementFactory.getRuntimeMXBean().getName();
            if (StringUtils.isBlank(processName)) {
                throw new Exception("Process name is blank!");
            }
            final int atIndex = processName.indexOf('@');
            int pid;
            if (atIndex > 0) {
                pid = Integer.parseInt(processName.substring(0, atIndex));
            } else {
                pid = processName.hashCode();
            }
            mpid.append(pid);
    
        } catch (Exception ex) {
            //ignore
        }
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }
  • 生成数据中心id。通过获取进程id,并配合机器id来生成
          /**
           * 获取数据中心ID<br>
           * 数据中心ID依赖于本地网卡MAC地址。
           * <p>
           * 此算法来自于mybatis-plus#Sequence
           * </p>
           *
           * @param maxDatacenterId 最大的中心ID
           * @return 数据中心ID
           */
          public static long getDataCenterId(long maxDatacenterId) {
              if(maxDatacenterId == Long.MAX_VALUE){
                  maxDatacenterId -= 1;
              }
              long id = 1L;
              SystemInfo systemInfo = new SystemInfo();
              String str = getDeviceId(systemInfo);
              byte[] mac = str.getBytes();
              if (null != mac) {
                  id = ((0x000000FF & (long) mac[mac.length - 2])
                          | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
                  id = id % (maxDatacenterId + 1);
              }
        
              return id;
          }
    
  • 充分利用序列号段,对于不在同一毫秒内的id,随机生成一个序列号
          public static long randomLong(final long limitExclude) {
              return ThreadLocalRandom.current().nextLong(limitExclude);
      }