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

只谈情不闲聊 提交于 2019-11-26 17:09:46

  最近在研究分布式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) 单线程循环取4096000个主键,刚好1004毫秒,说明没有性能问题。

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

 

4.2 源码地址

https://github.com/tonylau08/dcafe

如测试使用过程中发现任何错误,请告知。如觉得不错,给我颗小星星。谢谢!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!