分布式ID生成策略(1)_snowflake算法

转载:

最近在研究分布式ID的生成方法,发现Twitter的snowflake算法挺有意思,因此亲自动手用Java进行了实现。

 

snowflake算法的原理就是用64位整数来表示主键,其结构如下图:

1 bit符号位:设计者不喜欢负数主键?方便使用负数标识不正确的ID?

41 bit毫秒时间:2^41 / (365 * 24 * 3600 * 1000) ≈ 69年

10 bit机房ID + 机器ID:最大值为1023

12 bit递增序列:最大值为4095

 

因为使用机房ID + 机器ID来标识机器,因此可以分散到每台业务机器运行而不会产生重复,不需要集中产生主键,这是这个算法最大的优点。

每秒最多可以生成主键数:4096 * 1000毫秒 = 4096000。以当前机器的配置情况和业务情况,单机每秒400万不重复ID无论如何都已经足够。

虽然算法本身很简单,但分布式集群面临的情况很复杂,编码过程中要考虑的因素有很多。废话不多说,“翠花!上代码!”

 

1.0 分布式时间发生器

1.1 设计考虑

(1) System.currentTimeMillis()方法每次执行都要进行一次系统内核调用,系统开销较大。对于当前的这个序列号生成器来说,只要保证递增序列从4095归0时获取的时间 比 上次归0时获取的时间大就不会产生重复值,因此使用一个long变量缓存了最近一次时间。

(2) 机房ID 和 机器ID正常情况下不会发生改变,因此每次从系统更新时间后立即进行或运算并保存,避免频繁的更新操作。

(3) 配置类AbstractRMConfig 设计成抽象类,用户可自由实现并注册到时间发生器即可。

(4) 为避免业务平静期递增序列长时间无法到达4096,导致缓存时间过旧引发其它问题,因此使用定时线程TimeUpdater每1000毫秒更新一次时间,间隔时间可以自由设置。

1.2 代码

/**
 * 分布式时间发生器
 * @author Tony.Lau
 */
public enum TimeGenerator {
 
    INSTANCE;
 
    private Logger logger = LoggerFactory.getLogger(TimeGenerator.class);
 
    private AbstractRMConfig config;
    private long lastTimeMills;
    private volatile boolean isFail = true;
    private int rmid = -1;
 
    private final Lock rmidLock = new ReentrantLock();
 
    private ScheduledExecutorService es = Executors.newScheduledThreadPool(1);
    private boolean isRun = false;
 
    /** 获取缓存时间 */
    long getTime() {
        try {
            rmidLock.lock();
            if (isFail) {
                return -1l;
            }
            return lastTimeMills;
        } finally {
            rmidLock.unlock();
        }
    }
 
    /** 获取最新时间 */
    long updateTime() {
        try {
            rmidLock.lock();
            if (isFail) {
                return -1l;
            }
            long temp = (System.currentTimeMillis() << 23 >>> 1) ^ rmid;
            while (temp <= lastTimeMills) {
                temp = (System.currentTimeMillis() << 23 >>> 1) ^ rmid;
            }
            return lastTimeMills = temp;
        } finally {
            rmidLock.unlock();
        }
    }
 
    /** 注册配置信息 */
    public RegisterState registerRoomMachine(AbstractRMConfig config) {
        isFail = true;
        if (config == null) {
            return RegisterState.ERROR;
        }
        if (config instanceof FailRMConfig) {
            return RegisterState.FAIL;
        }
        try {
            rmidLock.lock();
            this.config = config;
            if (!updateRmid().equals(RegisterState.OK)) {
                logger.error("registerRoomMachine error");
                return RegisterState.ERROR;
            }
            if (!isRun) {
                int timePeriod = config.getTimeUpdatePeriod();
                if(timePeriod < 1){
                    logger.error("getTimeUpdatePeriod error:" + timePeriod + "<1");
                    return RegisterState.ERROR;
                }
                es.scheduleAtFixedRate(new TimeUpdater(), 0, timePeriod, TimeUnit.MILLISECONDS);
                isRun = true;
            }
            isFail = false;
        } finally {
            rmidLock.unlock();
        }
        logger.info("registerRoomMachine success");
        return RegisterState.OK;
    }
 
    /** 更新机房ID 和 机器ID */
    private RegisterState updateRmid() {
        logger.debug("updateRmid()");
        int roomId = config.getRoomId();
        int roomBitNum = config.getRoomBitNum();
 
        int machineId = config.getMachineId();
        int machineBitNum = config.getMachineBitNum();
 
        if (roomId < 0 || machineId < 0) {
            isFail = true;
            logger.error("房间ID 或 机器ID不能小于0:roomId=" + roomId + "--machineId=" + machineId);
            return RegisterState.ERROR;
        }
 
        if (roomBitNum < 1 || machineBitNum < 1) {
            isFail = true;
            logger.error("房间ID位数 或 机器ID位数不能小于1:roomBitNum=" + roomBitNum + "--machineBitNum=" + machineBitNum);
            return RegisterState.ERROR;
        }
 
        if (roomBitNum + machineBitNum > 10) {
            isFail = true;
            logger.error("房间ID+机器ID组合后位数不能超过10位:roomBitNum=" + roomBitNum + "--machineBitNum=" + machineBitNum);
            return RegisterState.ERROR;
        }
        if (roomId >= (1 << roomBitNum)) {
            isFail = true;
            logger.error("机房ID超过设定数值:" + roomId + ">=" + (1 << roomBitNum));
            return RegisterState.ERROR;
        }
        if (machineId >= (1 << machineBitNum)) {
            isFail = true;
            logger.error("机器ID超过设定数值" + machineId + ">=" + (1 << machineBitNum));
            return RegisterState.ERROR;
        }
 
        rmid = ((roomId << machineBitNum) ^ machineId) << 12;
        lastTimeMills = (System.currentTimeMillis() << 23 >>> 1) ^ rmid;
        return RegisterState.OK;
    }
 
    /**
     * <b>注册状态</b><br>
     * OK:注册机房ID和机器ID成功,可以开始获取主键。<br>
     * FAIL:注册Fail对象成功,系统停止产生正确主键,全部返回-1。<br>
     * ERROR:注册机房ID和机器ID失败,空对象或者参数错误,系统无法产生正确主键,全部返回-1。<br>
     * 
     * @create 2016-12-22 21:06:35
     */
    public enum RegisterState {
        OK, FAIL, ERROR;
    }
 
    /**
     * <b>时间定时更新器</b><br>
     * @create 2016-12-22 22:09:45
     */
    private class TimeUpdater implements Runnable {
 
        @Override
        public void run() {
            try {
                updateTime();
            } catch (Exception e) {
                logger.error("定时更新时间发生错误", e);
            }
        }
    }
 
}


2.0 分布式自增长主键发生器

2.1 设计考虑

(1) 多表共用一个实例,避免连锁更新时间和代码复杂化。

(2) 每次增长到4096就归0并更新到最新时间,其它取缓存时间。

(3) 有文章说每次归0会导致0过多,Hash取模分表后0表的数据会偏多。但似乎并不会,因此没有采用随机数发生器。

2.2 代码

/**
 * <b>分布式自增长主键发生器</b><br>
 * 枚举单例,多表公用一个实例。
 * @author Tony.Lau
 * @create 2016-12-23 09:50:41
 */
public enum PrimaryKeyGen {
    
    INSTANCE;
 
    private final Lock INCR_LOCK = new ReentrantLock();
    private int increment = 0;
    
    /**
     * <b>1bit符号位 + 41bit时间 + 机房ID + 机器ID + 12bit自增长ID</b><br>
     * @return 如果返回值小于等于0,则表示系统环境错误;大于0为正常值。
     */
    public long getIncrKey() {
        try {
            INCR_LOCK.lock();
            long time = 0l;
            if (increment >= 4096) {
                increment = 0;
                if((time = TimeGenerator.INSTANCE.updateTime()) < 0){
                    return -1l;
                }else{
                    return time ^ (increment++);
                }
            }else{
                if((time = TimeGenerator.INSTANCE.getTime()) < 0){
                    return -1l;
                }else{
                    return time ^ (increment++);
                }
            }
        } finally {
            INCR_LOCK.unlock();
        }
    }
    
}


3.0 使用示例

3.1 使用步骤

(1) 实现具体的配置类,譬如从配置文件获取配置信息,从zookeeper在线获取配置信息。

(2) 匿名静态代码块注册配置信息到时间发生器,然后就可以正常获取主键。

(3) 如果使用Spring容器,可以使用@Postconstruct初始化注册信息。

(4) 配置类的fail()方法:如发生异常情况,譬如与zookeeper失去连接,意味着节点可能被清理,其它机器上线后可能使用了相同的机器ID导致主键重复。因此可以在配置实现类中跟踪异常信息,并在异常出现时立刻调用fail()方法停止产生正确主键。

(5) 配置类的init()方法:如需要使用动态注册方式,可以将获取配置的代码在这里实现。

(6) 配置类的refresh()方法:如想动态扩容方便,运行期动态更新机器ID和机房ID,那么可以将实现放在这里。

  注意事项:如果机房内的机器时间有快有慢,那么当一台机器意外下线,另外一台机器上线抢占了相同ID,那么很大可能会产生重复主键。编程实现时一定要注意:

  ① 机器时间一定要尽可能一致。

  ② 新上线机器一段时间内不会抢占其它机器ID,哪怕其已经下线。

 

3.2 示例代码

/**
 * 使用示例
 * @author Tony.Lau
 */
public class Example{
    
    static {
        RoomMachineConfig config = new RoomMachineConfig(0, 1, 0, 1, 1000);
        RegisterState state = TimeGenerator.INSTANCE.registerRoomMachine(config);
    }
    
    private static PrimaryKeyGen keyGen = PrimaryKeyGen.INSTANCE;
    
    public long getKey(){
        return keyGen.getIncrKey();
    }
    
    private static class RoomMachineConfig extends AbstractRMConfig{
        
        public RoomMachineConfig(){
            this.init();
            /*
            if(config.change()){
                refresh();
            }
            */
        }
        
        public RoomMachineConfig(int roomId, int roomBitNum, int machineId, int machineBitNum, int timeUpdatePeriod) {
            super(roomId, roomBitNum, machineId, machineBitNum, timeUpdatePeriod);
            /*
            if(config.change()){
                refresh();
            }
            */
        }
        
        @Override
        protected RegisterState init() {
            // 获取配置并设置参数
            //this.roomId = 
            //this.roomBitNum = 
            //this.machineId = 
            //this.machineBitNum = 
            return TimeGenerator.INSTANCE.registerRoomMachine(this);
        }
        
        @Override
        protected RegisterState refresh() {
            //  获取配置并更新参数
            //this.roomId = 
            //this.roomBitNum = 
            //this.machineId = 
            //this.machineBitNum = 
            return TimeGenerator.INSTANCE.registerRoomMachine(this);
        }
 
        @Override
        public int getRoomId() {
            return roomId;
        }
 
        @Override
        public int getRoomBitNum() {
            return roomBitNum;
        }
 
        @Override
        public int getMachineId() {
            return machineId;
        }
 
        @Override
        public int getMachineBitNum() {
            return machineBitNum;
        }
        
        @Override
        public int getTimeUpdatePeriod(){
            return timeUpdatePeriod;
        }
 
    }
    
}


4.0 其它事项

4.1 测试结果

(1) 单线程循环取409600个主键,刚好1004毫秒,说明没有性能问题。

(2) 多线程分别循环取409600个主键,用时2248毫秒,未发现重复值。

 

4.2 源码地址

https://github.com/tonylau08/dcafe
--------------------- 
作者:coffeelifelau 
来源:CSDN 
原文:https://blog.csdn.net/coffeelifelau/article/details/53856032 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值