最近在研究分布式ID的生成方法,发现Twitter的snowflake算法挺有意思,所以亲自动手用Java进行了实现。java
snowflake算法用64位整数来表示主键,其结构以下图:git
1 bit符号位:设计者不喜欢负数主键?方便使用负数标识不正确的ID?github
41 bit毫秒时间:2^41 / (365 * 24 * 3600 * 1000) ≈ 69年算法
10 bit机房ID + 机器ID:最大值为1023编程
12 bit递增序列:最大值为4095缓存
由于使用机房ID + 机器ID来标识机器,所以能够分散到每台业务机器运行而不会产生重复,不须要集中产生主键,这是这个算法最大的优势。多线程
每秒最多能够生成主键数:4096 * 1000毫秒 = 4096000。以当前机器的配置状况和业务状况,单机每秒400万不重复ID不管如何都已经足够。分布式
虽然算法自己很简单,但分布式集群面临的状况很复杂,编码过程当中要考虑的因素有不少。废话很少说,“翠花!上代码!”ide
(1) System.currentTimeMillis()方法每次执行都要进行一次系统内核调用,系统开销较大。对于当前的这个序列号生成器来讲,只要保证递增序列从4095归0时获取的时间 比 上次归0时获取的时间大就不会产生重复值,所以使用一个long变量缓存了最近一次时间。性能
(2) 机房ID 和 机器ID正常状况下不会发生改变,所以每次从系统更新时间后当即进行或运算并保存,避免频繁的更新操做。
(3) 配置类AbstractRMConfig 设计成抽象类,用户可自由实现并注册到时间发生器便可。
(4) 为避免业务平静期递增序列长时间没法到达4096,致使缓存时间过旧引起其它问题,所以使用定时线程TimeUpdater每1000毫秒更新一次时间,间隔时间能够自由设置。
/** * 分布式时间发生器 * @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); } } } }
(1) 多表共用一个实例,避免连锁更新时间和代码复杂化。
(2) 每次增加到4096就归0并更新到最新时间,其它取缓存时间。
(3) 有文章说每次归0会致使0过多,Hash取模分表后0表的数据会偏多。但彷佛并不会,所以没有采用随机数发生器。
/** * <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(); } } }
(1) 实现具体的配置类,譬如从配置文件获取配置信息,从zookeeper在线获取配置信息。
(2) 匿名静态代码块注册配置信息到时间发生器,而后就能够正常获取主键。
(3) 若是使用Spring容器,可使用@Postconstruct初始化注册信息。
(4) 配置类的fail()方法:如发生异常状况,譬如与zookeeper失去链接,意味着节点可能被清理,其它机器上线后可能使用了相同的机器ID致使主键重复。所以能够在配置实现类中跟踪异常信息,并在异常出现时马上调用fail()方法中止产生正确主键。
(5) 配置类的init()方法:如须要使用动态注册方式,能够将获取配置的代码在这里实现。
(6) 配置类的refresh()方法:如想动态扩容方便,运行期动态更新机器ID和机房ID,那么能够将实现放在这里。
注意事项:若是机房内的机器时间有快有慢,那么当一台机器意外下线,另一台机器上线抢占了相同ID,那么很大可能会产生重复主键。编程实现时必定要注意:
① 机器时间必定要尽量一致。
② 新上线机器一段时间内不会抢占其它机器ID,哪怕其已经下线。
/** * 使用示例 * @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; } } }
(1) 单线程循环取4096000个主键,恰好1004毫秒,说明没有性能问题。
(2) 多线程分别循环取4096000个主键,用时2248毫秒,未发现重复值。
https://github.com/tonylau08/dcafe
如测试使用过程当中发现任何错误,请告知。如以为不错,给我颗小星星。谢谢!