分布式全局唯一ID的原理及实现

1:实现原理及介绍:

 

全局唯一ID生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。

Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占10比特)+ 自增值(占12比特),总共64比特组成的一个Long类型。

  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。

  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年

  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。

  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID

  • 其中工作机器ID共有2的10次方1024个 每个实例程序启动的时候进行分配一个机器编号,每台机器生成的唯一ID可以支持每秒409万并发,理论上最大可以支持41.948亿并发。
  • 每台实例启动时会从redis里分配一个机器编号(redis宕机会随机分配),存储在redis中设置过期时间,结构为,在过期之前或进行续命保活操作,同事在服务列表中去注册上,服务列表存储在redis中 结构为 hsah  value为map结构,具体位置 listkey-→ 同事也会定期去redis中维护服务列表,剔除列表中失活的机器编号,这样可以进行再分配,保证列表中都为活着的机器。

2:源码实现

package com.example.demo;

import com.zkh360.zaf.lock.GlobalLock;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author wei.gu
 * @project_name sp
 * @create 2020-04-13 13:47
 * *  利用SnowFlake算法,生成有个全局唯一的编号信息
 * Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。
 * Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特组成的一个Long类型。
 * 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
 * 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
 * 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。
 * 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID
 **/
@Slf4j
@Component
public class GlobalNumberUtil implements CommandLineRunner {


    /**
     * 起始的时间戳
     */
    private final static long START_TIMESTAMP = 1480166465631L;

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12;   //序列号占用的位数
    private final static long MACHINE_BIT = 5;     //机器标识占用的位数
    private final static long DATA_CENTER_BIT = 5; //数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

    private static long dataCenterId = 1L;  //数据中心
    private static long machineId;     //机器标识
    private static long sequence = 0L; //序列号
    private static long lastTimeStamp = -1L;  //上一次时间戳

    private static String machinekey = "com:qishucai:machineId";
    private static String dataCenterKey = "com:qishucai:dataCenterID";
    private static StringBuilder sb = new StringBuilder("ip:pid:dataCenterKey:machinekey:");
    private static String aliveListKey = "com:qishucai:aliveList";
    private static String machineAndDataCenterKey;

    private final static  int MAXCOMBINATION = 1024; //数据中心编号和机器编号的最大组合
    private final static  int END = 1;//重试拿不重复的数据中心编号和机器编号的结束标志

    @Resource
    RedisTemplate redisTemplate;


    private static long getNextMill() {
        long mill = getNewTimeStamp();
        while (mill <= lastTimeStamp) {
            mill = getNewTimeStamp();
        }
        return mill;
    }

    private static long getNewTimeStamp() {
        return System.currentTimeMillis();
    }

    /**
     * 根据指定的数据中心ID和机器标志ID生成指定的序列号
     *
     * @param dataCenterId 数据中心ID
     * @param machineId    机器标志ID
     */
    private GlobalNumberUtil(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
        }
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }

    /**
     * 根据指定的数据中心ID和机器标志ID生成指定的序列号
     */
    public GlobalNumberUtil() {


    }


    /**
     * 产生下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currTimeStamp = getNewTimeStamp();
        if (currTimeStamp < lastTimeStamp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currTimeStamp == lastTimeStamp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currTimeStamp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }


        lastTimeStamp = currTimeStamp;
        return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分
            | dataCenterId << DATA_CENTER_LEFT       //数据中心部分
            | machineId << MACHINE_LEFT             //机器标识部分
            | sequence;

    }

    /**
     * 根据传过来的位数生成对应位数的唯一编号
     *
     * @return
     */
    public synchronized long nextId(int numberCount) {
        if (numberCount > 31 || numberCount < 0) {
            throw new IllegalArgumentException("NumberCount can't be greater than 31 or less than 0");
        }
        long id = nextId();
        String strID = new String(id + "");
        return Long.valueOf(strID.substring(strID.length() - numberCount, strID.length()));
    }

    /**
     * Callback used to run the bean.
     *
     * @param args incoming main method arguments
     * @throws Exception on error
     */
    @Override
    public void run(String... args) throws Exception {

        String ip = null;
        String pid = null;
        try {
            ip = InetAddress.getLocalHost().getHostAddress();
            String processName = ManagementFactory.getRuntimeMXBean().getName();
            pid = processName.substring(0, processName.indexOf('@'));

        } catch (UnknownHostException e) {
            log.error("获取IP地址出错");
            e.printStackTrace();
        }
        //获取存活列表 添加新的额 机器编号 数据中心编号对
        Map map = hmget(aliveListKey);
        if (map == null) {
            map = new HashMap<>();
        }

        boolean flag = true;
        int endFlag = map.size() + 1;
        NumberEntry numberEntry = null;

        if (map.size() < MAXCOMBINATION) {//数据中心编号 和机器编号 只能有1024种组合
            while (flag) {
                numberEntry = getNumberEntry();
                for (Map.Entry entry : map.entrySet()) {
                    NumberEntry value = (NumberEntry) entry.getValue();
                    if (value.getMachineId() == numberEntry.getMachineId() && value.getMachineId() == numberEntry.getMachineId()) {
                        endFlag = 0;
                        log.info("生成的数据中心编号为= {} 生成的机器ID为= {} 已经存在重新获取", dataCenterId, machineId);
                        break;
                    }
                    endFlag--;
                }

                if (endFlag == END) {

                    flag = false;
                }
            }
        }

        log.info("生成的数据中心编号为= {} 生成的机器ID为= {} 来源为= {}", dataCenterId, machineId, numberEntry.source == true ? "redis生成" : "随机生成");
        machineAndDataCenterKey = sb.append(ip).append("-").append(pid).append("-").append(dataCenterId).append("-").append(machineId).toString();
        map.put(machineAndDataCenterKey, numberEntry);
        hmset(aliveListKey, map);
        log.info("成功更新了机器列表集体信息为:key= {} value= {}", machineAndDataCenterKey, map);

        //本机维护自己的 编号对一直存活下去,需要在过期之前续命
        set(machineAndDataCenterKey, numberEntry, 16);
        log.info("本机维护自己的 初始续命成功具体信息为 key = {}  value = {}", machineAndDataCenterKey, numberEntry);


    }

    private NumberEntry getNumberEntry() {
        Boolean flag = true;
        try {
            machineId = incrForMachineId(machinekey);
            dataCenterId = incrForDataCenterId(dataCenterKey, machineId);
        } catch (Exception e) {
            flag = false;
            Random random = new Random();
            machineId = (long) random.nextInt(32);
            dataCenterId = (long) random.nextInt(32);
            e.printStackTrace();
        }
        return new NumberEntry(dataCenterId, machineId, flag);
    }

    /**
     * 内部类用来存储 机器编号 和数据中心编号
     *
     * @return
     */

    private static class NumberEntry implements Serializable {
        private long dataCenterId;  //数据中心
        private long machineId;     //机器标识
        private boolean source = true;     //来源标识 true 代表redis生成 false代表random生成

        public NumberEntry() {

        }

        @Override
        public String toString() {
            return "{" +
                "dataCenterId=" + dataCenterId +
                ", machineId=" + machineId +
                ", source=" + source +
                '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            NumberEntry that = (NumberEntry) o;

            if (dataCenterId != that.dataCenterId) return false;
            if (machineId != that.machineId) return false;
            return source == that.source;
        }

        @Override
        public int hashCode() {
            int result = (int) (dataCenterId ^ (dataCenterId >>> 32));
            result = 31 * result + (int) (machineId ^ (machineId >>> 32));
            result = 31 * result + (source ? 1 : 0);
            return result;
        }

        public long getDataCenterId() {
            return dataCenterId;
        }

        public void setDataCenterId(long dataCenterId) {
            this.dataCenterId = dataCenterId;
        }

        public long getMachineId() {
            return machineId;
        }

        public void setMachineId(long machineId) {
            this.machineId = machineId;
        }

        public boolean isSource() {
            return source;
        }

        public void setSource(boolean source) {
            this.source = source;
        }

        public NumberEntry(long dataCenterId, long machineId, boolean source) {
            this.dataCenterId = dataCenterId;
            this.machineId = machineId;
            this.source = source;
        }
    }

    /**
     * 在过期之前进行重新放 保证在程序运行时一直存在
     *
     * @return
     */
    @Scheduled(cron = "0/9 * * * * ?")
    public void keepAliveBeforExpire() {

        //本机维护自己的 编号对一直存活下去,需要在过期之前续命
        NumberEntry numberEntry = (NumberEntry) get(machineAndDataCenterKey);
        if (Objects.nonNull(numberEntry)) {

            set(machineAndDataCenterKey, numberEntry, 16);
            log.info("本机维护自己的 续命成功具体信息为 key=  {}  value=  {}", machineAndDataCenterKey, numberEntry);
        } else {
            log.info("本机维护自己的 续命结束 对应的key=  {}", machineAndDataCenterKey);

        }

    }

    /**
     * 保存的服务器存活列表 会定期进行检查是否存活 存活的话就保存在名单里
     * 如果已经失活就从列表中剔除
     *
     * @return
     */
    @Scheduled(cron = "0 */1 * * * ?")
    @GlobalLock
    public void aliveList() {
        Map map = hmget(aliveListKey);
        for (Map.Entry entry : map.entrySet()) {
            Object value = get(entry.getKey().toString());
            if (value == null) {
                hdel(aliveListKey, entry.getKey().toString());
                log.info("定期从存活名单列表里剔除已死亡的服务:key=  {} value=  {}", entry.getKey().toString(), entry.getValue().toString());
            } else {

                log.info("最近机器列表中存活的机器信息为:key=  {} value=  {}", entry.getKey().toString(), entry.getValue().toString());
            }
        }
    }

    /**
     * 获得机器编号 范围0-31
     *
     * @param key 键
     * @return
     */
    private long incrForMachineId(String key) {

        Long increment = redisTemplate.opsForValue().increment(key, 1);
        if (increment == null || increment > 31 || increment < 0) {
            redisTemplate.opsForValue().set(key, -1);
        }
        return redisTemplate.opsForValue().increment(key, 1);
    }

    /**
     * 获得数据中心编号 范围0-31
     *
     * @param key 键
     * @return
     */
    private long incrForDataCenterId(String key, long machineId) {

        Long increment = new Long(redisTemplate.opsForValue().get(key).toString());
        if (increment == null || increment > 31 || increment < 0) {
            redisTemplate.opsForValue().set(key, 0);
        }
        if (machineId > 31) {

            return new Long(redisTemplate.opsForValue().increment(key, 1).toString());
        } else {
            return new Long(redisTemplate.opsForValue().get(key).toString());
        }
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    private Map hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    private void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    private boolean hmset(String key, Map map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    private boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    private boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    private Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

}

   

      

你可能感兴趣的:(经验杂谈,java~~基础)