1. 程式人生 > >分散式全域性id生成原始碼

分散式全域性id生成原始碼

package com.jd.medicine.base.common.global.id;

import com.jd.jim.cli.Cluster;
import com.jd.medicine.base.common.logging.LogUtil;
import com.jd.medicine.base.common.util.DateUtil;
import com.jd.medicine.base.common.util.IpUtil;
import com.jd.medicine.base.common.util.StringUtil;
import com.jd.ump.profiler.proxy.Profiler;
import org.slf4j.Logger;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

/**
 * 功能描述:雪花演算法生成全域性唯一id
 *
 * @author yaoyizhou
 * @date 2018/11/06 10:15
 * @desc
 */
public class MachineIdRegist {
    private static Logger logger = LogUtil.getLogger(MachineIdRegist.class);

    /**
     * redis例項
     */
    private Cluster jimClient;

    /**
     * 機器碼key前面一段
     */
    private String machineIdRedisKey;

    /**
     * 機器id
     */
    private static Integer machine_id;

    /**
     * 本地ip地址
     */
    private String localIp;

    /**
     * 可以註冊的機器個數64個
     */
    private Integer MACHINE_ID_NUM = 64;

    /**
     * 配置項
     * 初始化redis
     *
     * @param jimClient
     */
    public void setJimClient(Cluster jimClient) {
        this.jimClient = jimClient;
    }

    /**
     * 配置項
     * 初始化機器id key的字首
     *
     * @param machineIdRedisKey
     */
    public void setMachineIdRedisKey(String machineIdRedisKey) {
        this.machineIdRedisKey = machineIdRedisKey;
    }

    /**
     * 配置項
     * 初始化時呼叫
     * 作用:
     * 1.對當前機器ip去掉“.”並轉化成long型別
     * 2.hash機器初始化一個機器ID
     */
    public void initMachineId() {
        if (StringUtil.isBlank(machineIdRedisKey)) {
            logger.info("machineIdRedisKey 不能為空");
            throw new RuntimeException("machineIdRedisKey is null ,please init first");
        }
        try {
            localIp = IpUtil.getInet4Address();
        } catch (Exception e) {
            //如果拋異常了,直接給一個固定的ip
            throw new RuntimeException("全域性id機器碼註冊異常!");
        }
        Long ip_ = Long.parseLong(localIp.replaceAll("\\.", ""));
        //這裡取64,為後續機器Ip調整做準備。
        machine_id = (int) (ip_ % MACHINE_ID_NUM);
        //建立一個機器ID
        createMachineId();
        logger.info("初始化 machine_id :{}", machine_id);
        SnowFlakeGenerator.initMachineId(machine_id);
    }

    /**
     * 配置項
     * 容器銷燬前呼叫
     * 作用:清除註冊記錄
     */
    public void destroyMachineId() {
        jimClient.del(machineIdRedisKey + machine_id);
    }

    /**
     * 主方法:獲取一個機器id
     *
     * @return
     */
    private Integer createMachineId() {
        try {
            //向redis註冊,並設定超時時間
            Boolean aBoolean = registMachine(machine_id);
            //註冊成功
            if (aBoolean) {
                //啟動一個執行緒更新超時時間
                updateExpTimeThread();
                //返回機器Id
                return machine_id;
            }
            //檢查是否被註冊滿了.不能註冊,就直接返回,如果能註冊,則直接給machine_id 賦值返回
            if (!checkIfCanRegist()) {
                //註冊滿了,加一個報警,然後拋異常
                throw new RuntimeException("全域性id機器碼註冊滿,10分鐘");
            }
            //機器碼+1
            incMachineId();
            logger.info("createMachineId->ip:{},machineId:{}, time:{}", localIp, machine_id, DateUtil.getDate("yyyy-MM-dd HH:mm:ss"));
            //遞迴呼叫
            createMachineId();
        } catch (Exception e) {
            throw new RuntimeException("全域性id機器碼註冊異常!");
        }
        return machine_id;
    }

    /**
     * 檢查是否被註冊滿了
     *
     * @return
     */
    private Boolean checkIfCanRegist() {
        //判斷0~MACHINE_ID_NUM這個區間段的機器IP是否被佔滿
        for (int i = 0; i <= (MACHINE_ID_NUM-1); i++) {
            Boolean flag = jimClient.exists(machineIdRedisKey + i);
            //如果不存在。說明還可以繼續註冊。直接返回i
            if (!flag) {
                return true;
            }
        }
        return false;
    }

    /**
     * 1.更新超時時間
     * 注意,更新前檢查是否存在機器ip佔用情況
     * 例如:當前ip的註冊資訊丟失,這時
     */
    private void updateExpTimeThread() {
        //開啟一個執行緒執行定時任務:
        //1.超時時間10分鐘,8分鐘更新一次超時時間
        new Timer(localIp).schedule(new TimerTask() {
            @Override
            public void run() {
                //檢查快取中的ip與本機ip是否一致,一致則更新時間,不一致則重新取一個機器ID
                Boolean b = checkIsLocalIp(String.valueOf(machine_id));
                if (b) {
                    logger.info("更新超時時間 ip:{},machineId:{}, time:{}", localIp, machine_id, DateUtil.getDate("yyyy-MM-dd HH:mm:ss"));
                    jimClient.expire(machineIdRedisKey + machine_id, 10, TimeUnit.MINUTES);
                } else {
                    logger.info("重新生成機器ID ip:{},machineId:{}, time:{}", localIp, machine_id, DateUtil.getDate("yyyy-MM-dd HH:mm:ss"));
                    //重新生成機器ID,並且更改雪花中的機器ID
                    initMachineId();
                    //重新生成並註冊機器id
                    createMachineId();
                    //更改雪花中的機器ID
                    SnowFlakeGenerator.initMachineId(machine_id);
                    //結束當前任務
                    logger.info("Timer->thread->name:{}", Thread.currentThread().getName());
                    this.cancel();
                }
            }
        }, 10 * 1000, 1000 * 60 * 8);
    }

    /**
     * 獲取1~MACHINE_ID_NUM隨機數
     */
    private void getRandomMachineId() {
        machine_id = (int) (Math.random() * MACHINE_ID_NUM);
    }

    /**
     * 機器ID順序獲取
     */
    private void incMachineId() {
        logger.info("incMachineId->id-1:{}", machine_id);
        if (machine_id >= MACHINE_ID_NUM) {
            machine_id = 0;
        } else {
            machine_id += 1;
        }
        logger.info("incMachineId->id:{}", machine_id);
    }

    /**
     * @param mechineId
     * @return
     */
    private Boolean checkIsLocalIp(String mechineId) {
        String ip = jimClient.get(machineIdRedisKey + mechineId);
        logger.info("checkIsLocalIp->ip:{}", ip);
        return localIp.equals(ip);
    }

    /**
     * 1.註冊機器
     * 2.設定超時時間
     *
     * @param mechineId 取值為0~MACHINE_ID_NUM
     * @return
     */
    private Boolean registMachine(Integer mechineId) throws Exception {
        return jimClient.set(machineIdRedisKey + mechineId, localIp, 10, TimeUnit.MINUTES, false);
    }
}

package com.jd.medicine.base.common.global.id;

import com.jd.medicine.base.common.logging.LogUtil;
import org.slf4j.Logger;

import java.util.Random;

/**
 * 功能描述:
 *
 *      每一部分佔用位數的預設值 雪花演算法利用了64長度,我們計劃利用 1+40+6+7=53
 *      1佔位+40位時間戳+6位機器碼+7位隨機數
 *
 *      1.可以使用(2^40 −1)/(1000∗60∗60∗24∗365)=34.8年
 *      2.生成最長16位id
 *      3.理論上支援每毫秒生成的id個數支援128個。
 *      4.支援叢集中機器個數64個。
 *
 *      參考:https://segmentfault.com/a/1190000011282426#articleHeader2
 *
 * @author yaoyizhou
 * @date 2018/11/7 16:09
 * @desc
 */
public class SnowFlakeGenerator {
    private static Logger logger = LogUtil.getLogger(SnowFlakeGenerator.class);

    /**
     * 機器碼佔用的位數 原雪花演算法預設是5,我們不需要,用6
     */
    private final static int MACHINE_BIT_NUM = 6;

    /**
     * 資料中心佔用的位數 原雪花演算法預設是5,我們不需要,用0
     */
    private final static int DATACENTER_BIT_NUM = 0;

    /**
     * 隨機數的長度,採用雪花演算法的預設值 12,我們用7,表示0-128
     */
    private final static int SEQUENCE_BIT_NUM = 7;

    /**
     * 起始的時間戳
     * 寫程式碼時的時間戳
     * 2018-11-07 16:06:00
     */
    private final static long START_STAMP = 1541577921683L;

    /**
     * 用於進行末尾sequence隨機數產生
     */
    private final static Random RANDDOM = new Random();

    private static final SnowFlakeGenerator single = new SnowFlakeGenerator();

    //靜態工廠方法
    public static SnowFlakeGenerator getInstance() {
        if (single.machineId < 0) {
            throw new RuntimeException("machineId less 0,please init first");
        }
        return single;
    }

    protected static void initMachineId(Integer mechineId) {
        logger.info("initMachineId->mechineId:{}", mechineId);
        single.setMachineId(Integer.valueOf(mechineId));
    }

    /**
     * datacenter編號 我們不用,預設值為0
     */
    private long datacenterId = 0;

    /**
     * 機器編號
     */
    private volatile long machineId = -1;

    /**
     * 當前序列號
     */
    private long sequence = 0L;

    /**
     * 上次最新時間戳
     */
    private long lastStamp = -1L;

    /**
     * datacenter偏移量:一次計算出,避免重複計算
     */
    private int idcBitLeftOffset;

    /**
     * 機器id偏移量:一次計算出,避免重複計算
     */
    private int machineBitLeftOffset;

    /**
     * 時間戳偏移量:一次計算出,避免重複計算
     */
    private int timestampBitLeftOffset;

    /**
     * 最大序列值掩碼,防止移除:一次計算出,避免重複計算
     */
    private Long maxSequenceValue;

    private SnowFlakeGenerator() {
        this.maxSequenceValue = -1L ^ (-1L << SEQUENCE_BIT_NUM);
        machineBitLeftOffset = SEQUENCE_BIT_NUM;
        idcBitLeftOffset = MACHINE_BIT_NUM + SEQUENCE_BIT_NUM;
        timestampBitLeftOffset = DATACENTER_BIT_NUM + MACHINE_BIT_NUM + SEQUENCE_BIT_NUM;
    }

    private void setMachineId(int machineId) {
        if (machineId < 0 || machineId > (-1 ^ (-1 << MACHINE_BIT_NUM))) {
            throw new RuntimeException("machineID less 0 or outOf maxMachineId," + machineId);
        }
        this.machineId = machineId;
    }

    /**
     * 產生下一個ID
     */
    public synchronized long nextId() {
        long currentStamp = getTimeMill();
        if (currentStamp < lastStamp) {
            throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastStamp - currentStamp));
        }
        //相同或不同,我們都序列自增
        if (currentStamp == lastStamp) {
            sequence = (sequence + 1);
            //如果自增序列超過了最大值,就返回到0,由於可能和之前序列重複,因此阻塞一毫秒,避免重複
            if (sequence > maxSequenceValue) {
                sequence = 0L;
                currentStamp = tilNextMillis();
            }
        } else {
            //時間不相同時,原雪花演算法是sequence歸0,但這樣造成大部分尾數都一樣,我們是取0-9隨機數,方便以id進行路由分片
            sequence = RANDDOM.nextInt(10);
        }

        lastStamp = currentStamp;
        logger.info("return snkowId:{}<<{}|{}<<{}|{}<<{}|{}", (currentStamp - START_STAMP), timestampBitLeftOffset, datacenterId, idcBitLeftOffset, machineId, machineBitLeftOffset, sequence);
        return (currentStamp - START_STAMP) << timestampBitLeftOffset | datacenterId << idcBitLeftOffset | machineId << machineBitLeftOffset | sequence;
    }

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

    private long tilNextMillis() {
        //在一毫秒的時間內,阻塞,直到下一毫秒
        long timestamp = getTimeMill();
        while (timestamp <= lastStamp) {
            timestamp = getTimeMill();
        }
        return timestamp;
    }

    public static void main(String args[]) {
        try {
            System.out.println(-1L ^ (-1L << 10));
            SnowFlakeGenerator.initMachineId(50);
            System.out.println("id:" + SnowFlakeGenerator.getInstance().nextId());
            Thread.sleep(2000);
            System.out.println("id:" + SnowFlakeGenerator.getInstance().nextId());
            Thread.sleep(2000);
            System.out.println("id:" + SnowFlakeGenerator.getInstance().nextId());
            Thread.sleep(2000);
            System.out.println("id:" + SnowFlakeGenerator.getInstance().nextId());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}