分布式ID生成策略(1)_snowflake算法
2017-08-28 13:54
417 查看
最近在研究分布式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无论如何都已经足够。
虽然算法本身很简单,但分布式集群面临的情况很复杂,编码过程中要考虑的因素有很多。废话不多说,“翠花!上代码!”
(2) 机房ID 和 机器ID正常情况下不会发生改变,因此每次从系统更新时间后立即进行或运算并保存,避免频繁的更新操作。
(3) 配置类AbstractRMConfig 设计成抽象类,用户可自由实现并注册到时间发生器即可。
(4) 为避免业务平静期递增序列长时间无法到达4096,导致缓存时间过旧引发其它问题,因此使用定时线程TimeUpdater每1000毫秒更新一次时间,间隔时间可以自由设置。
(2) 每次增长到4096就归0并更新到最新时间,其它取缓存时间。
(3) 有文章说每次归0会导致0过多,Hash取模分表后0表的数据会偏多。但似乎并不会,因此没有采用随机数发生器。
(2) 匿名静态代码块注册配置信息到时间发生器,然后就可以正常获取主键。
(3) 如果使用Spring容器,可以使用@Postconstruct初始化注册信息。
(4) 配置类的fail()方法:如发生异常情况,譬如与zookeeper失去连接,意味着节点可能被清理,其它机器上线后可能使用了相同的机器ID导致主键重复。因此可以在配置实现类中跟踪异常信息,并在异常出现时立刻调用fail()方法停止产生正确主键。
(5) 配置类的init()方法:如需要使用动态注册方式,可以将获取配置的代码在这里实现。
(6) 配置类的refresh()方法:如想动态扩容方便,运行期动态更新机器ID和机房ID,那么可以将实现放在这里。
注意事项:如果机房内的机器时间有快有慢,那么当一台机器意外下线,另外一台机器上线抢占了相同ID,那么很大可能会产生重复主键。编程实现时一定要注意:
① 机器时间一定要尽可能一致。
② 新上线机器一段时间内不会抢占其它机器ID,哪怕其已经下线。
(2) 多线程分别循环取4096000个主键,用时2248毫秒,未发现重复值。
如测试使用过程中发现任何错误,请告知。如觉得不错,给我颗小星星。谢谢!
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) 单线程循环取4096000个主键,刚好1004毫秒,说明没有性能问题。(2) 多线程分别循环取4096000个主键,用时2248毫秒,未发现重复值。
4.2 源码地址
https://github.com/tonylau08/dcafe如测试使用过程中发现任何错误,请告知。如觉得不错,给我颗小星星。谢谢!
相关文章推荐
- 分布式ID生成策略(1)_snowflake算法
- C语言实现分布式自增有序的唯一ID生成算法-snowflake算法
- 分布式ID生成策略
- 微信公号“架构师之路”学习笔记(四)-分布式ID生成算法(应用场景、uuid/guid、snowflake算法)
- 分布式ID生成策略
- 分布式系统全局id生成策略
- 数据库分库分表常见分布式主键ID生成策略
- 【Zanuck 镇】编写php高性能snowflake算法插件(分布式64位唯一性自增id生成算法)
- (三)架构篇之分布式id生成策略
- 分布式 ID 生成策略 —— 听云资深 Java 工程师
- 数据库分库分表(一)常见分布式主键ID生成策略
- C语言实现分布式自增有序的唯一ID生成算法-snowflake算法
- 常用的分布式ID生成策略
- 分布式 ID 生成策略
- C语言实现分布式自增有序的唯一ID生成算法-snowflake算法
- 分布式高并发下全局ID生成策略
- [架构师之路] 细聊分布式ID生成方法 2016-02-22
- 如何在高并发分布式系统中生成全局唯一Id
- Leaf——美团点评分布式ID生成系统
- 分布式系统唯一ID生成方案