Spring Cloud 微服务架构下的全局id生成器,基于tweet的snowflake算法改进

概述
  SnowFlake算法是Twitter设计的一个可以在分布式系统中生成唯一的ID的算法,它可以满足Twitter每秒上万条消息ID分配的请求,这些消息ID是唯一的且有大致的递增顺序。

原理
SnowFlake算法产生的ID是一个64位的整型,64个bit位被分成4段:

                                                                                 42--5--5--12

  42位时间戳部分,这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;41位(第一位是符号位)的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年;
5位数据中心序号标识
5位workerid标识,每个部署节点应有唯一的workerid标识
12位序列号部分,支持同一毫秒内同一个节点可以生成4096个ID;

SnowFlake算法生成的ID大致上是按照时间递增的,用在分布式系统中时,需要注意数据中心标识和部署节点必须唯一,这样就能保证每个节点生成的ID都是唯一的。

这个方案是Tweet提出,比较适合Tweet这种顶级的互联网公司,对于中小型互联网公司来说,还是不太适合的,理由如下:

1. 中小型的互联网公司服务一般部署在同个数据中心,要么在阿里云上,要么腾讯云上,因此数据中心序号可以去除。

2. 对于云原生的应用,同类服务要求部署docker镜像是完全一致的,那么workerid只能在docker启动时环境变量中指定,或者在外部配置文件中指定后让docker启动后读取,每次部署都要指定不一样的workerid,人工操作多了难免遗漏,以致不同的节点拥有相同的workerid,带来潜在的风险。

  因此对于云原生(cloud native)的应用,同类服务我们要追求镜像唯一,配置尽量精简,且尽量消除启动环境变量和application.yml外的配置文件的读取。

改进方案

1. 64位long型4段分为3段,去除datacenter id,10个bit全部赋予worker id
2. workerid无需指定,由zookeeper 临时有序类节点(EPHEMERAL_SEQUENTIAL)分配,具体的算法为:
  •     读取一个父node下所有的EPHEMERAL_SEQUENTIAL型节点,并把节点中的值(整形)读取出来,放入一个SET中
  •     for i=0 i<1023 i++
               如果i不在SET中,则选定当前的i值为新的workerid,循环终止
  •     在父node下创建一个新的EPHEMERAL_SEQUENTIAL,并把节点值设为刚选定workerid

    代码如下:
private long getUniqueWorkerId() {
		long retVal = -1;
		try {
			if(client.checkExists().forPath(parentNodePath) == null) {
				client.create().forPath(parentNodePath);
			}
			
			List children = client.getChildren().forPath(parentNodePath);
			Set idSet = new HashSet();
			for (String child : children) {
				byte[] data = client.getData().forPath(parentNodePath + "/" + child);
				String dataStr = new String(data);
				idSet.add(Long.parseLong(dataStr));
			}
			for (long i = 0; i < MAX_WORKER_ID; i++) {
				Long Longval = Long.valueOf(i);
				if (!idSet.contains(Longval)) {
					retVal = i;
					break;
				}
			}
			if (retVal == -1) {
				throw new RuntimeException("there is no spare workerid between 0 and " + MAX_WORKER_ID);
			}
			client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(parentNodePath+workerPathPrefix,
					(retVal + "").getBytes());
		} catch (Exception e) {
			LOGGER.error("Zookeeper operation failed.");
		}
		return retVal;
	}


3. 监控zookeeper的连接失去和重连事件,当zk连接丢失时,id生成器暂时是不能工作的,当zk重连成功时,id生成器会被重新分配一个workerid

代码如下:

private void init() {
		client = CuratorFrameworkFactory.newClient(zkConStr, 1000*60, 1000*15, new ExponentialBackoffRetry(1000, 3));
		client.start();
		long n = getUniqueWorkerId();
		this.workerId = n;
		client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
		    public void stateChanged(CuratorFramework client, ConnectionState newState) {
		    	if(newState == ConnectionState.LOST) {
		    		LOGGER.error(String.format(" the id generator with workid %d lost connection to zookeeper cluster.", workerId));
		    		IDGenerator.this.isNormal = false;
		        }
		        if(newState == ConnectionState.RECONNECTED) {
		        	LOGGER.info(String.format(" the id generator with workid %d reconnected to zookeeper cluster.", workerId));
		        	long n = getUniqueWorkerId();
		    		IDGenerator.this.workerId = n;
		    		IDGenerator.this.isNormal = true;
		        }
		    }
		});
		isInitialized = true;
		LOGGER.info(String.format(
				"worker starting. timestamp left shift %d, worker id bits %d, sequence bits %d, workerid %d",
				TIMESTAMP_LEFT_SHIFT,  WORKER_ID_BITS, SEQUENCE_BITS, workerId));
	}

完整核心类代码:

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class IDGenerator {
	private static final Logger LOGGER = LoggerFactory.getLogger(IDGenerator.class);
	private final static long TW_EPORCH = 1402974492729L;

	private final static long WORKER_ID_BITS = 10L;
	private final static long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
	private final static long SEQUENCE_BITS = 12L;

	private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;
	private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
	private final static long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);

	private static long LAST_TIMESTAMP = -1L;

	@Value("${idgenerator.zkConStr}")
	private String zkConStr;

	@Value("${idgenerator.parentNodePath}")
	private String parentNodePath;
	
	@Value("${idgenerator.workerPathPrefix}")
	private String workerPathPrefix;
	
	private CuratorFramework client;

	private volatile long workerId;
	private volatile long sequence = 0L;
	private volatile boolean isInitialized = false;
	private volatile boolean isNormal = true;
	
	private void init() {
		client = CuratorFrameworkFactory.newClient(zkConStr, 1000*60, 1000*15, new ExponentialBackoffRetry(1000, 3));
		client.start();
		long n = getUniqueWorkerId();
		this.workerId = n;
		client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
		    public void stateChanged(CuratorFramework client, ConnectionState newState) {
		    	if(newState == ConnectionState.LOST) {
		    		LOGGER.error(String.format(" the id generator with workid %d lost connection to zookeeper cluster.", workerId));
		    		IDGenerator.this.isNormal = false;
		        }
		        if(newState == ConnectionState.RECONNECTED) {
		        	LOGGER.info(String.format(" the id generator with workid %d reconnected to zookeeper cluster.", workerId));
		        	long n = getUniqueWorkerId();
		    		IDGenerator.this.workerId = n;
		    		IDGenerator.this.isNormal = true;
		        }
		    }
		});
		isInitialized = true;
		LOGGER.info(String.format(
				"worker starting. timestamp left shift %d, worker id bits %d, sequence bits %d, workerid %d",
				TIMESTAMP_LEFT_SHIFT,  WORKER_ID_BITS, SEQUENCE_BITS, workerId));
	}

	public synchronized long nextId() {
		if(!isNormal) {
			LOGGER.error(String.format(" the id generator with workid %d lost connection to zookeeper cluster, so it can not generate new id.", workerId));
			throw new RuntimeException("the id generator is out of service now, because it can not connect to zookeeper cluster!");
		}
		if(!isInitialized) {
			init();
		}
		long timestamp = System.currentTimeMillis();
		if (timestamp < LAST_TIMESTAMP) {
			LOGGER.error(String.format("clock is moving backwards.  Rejecting requests until %d.", LAST_TIMESTAMP));
			throw new RuntimeException(String.format(
					"Clock moved backwards.  Refusing to generate id for %d milliseconds", LAST_TIMESTAMP - timestamp));
		}
		if (LAST_TIMESTAMP == timestamp) {
			sequence = (sequence + 1) & SEQUENCE_MASK;
			if (sequence == 0) {
				timestamp = tilNextMillis(LAST_TIMESTAMP);
			}
		} else {
			sequence = 0L;
		}
		LAST_TIMESTAMP = timestamp;
		return ((timestamp - TW_EPORCH) << TIMESTAMP_LEFT_SHIFT) | (Long.valueOf(workerId) << WORKER_ID_SHIFT) | sequence;
	}

	private long getUniqueWorkerId() {
		long retVal = -1;
		try {
			if(client.checkExists().forPath(parentNodePath) == null) {
				client.create().forPath(parentNodePath);
			}
			
			List children = client.getChildren().forPath(parentNodePath);
			Set idSet = new HashSet();
			for (String child : children) {
				byte[] data = client.getData().forPath(parentNodePath + "/" + child);
				String dataStr = new String(data);
				idSet.add(Long.parseLong(dataStr));
			}
			for (long i = 0; i < MAX_WORKER_ID; i++) {
				Long Longval = Long.valueOf(i);
				if (!idSet.contains(Longval)) {
					retVal = i;
					break;
				}
			}
			if (retVal == -1) {
				throw new RuntimeException("there is no spare workerid between 0 and " + MAX_WORKER_ID);
			}
			client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(parentNodePath+workerPathPrefix,
					(retVal + "").getBytes());
		} catch (Exception e) {
			LOGGER.error("Zookeeper operation failed.");
		}
		return retVal;
	}

	private long tilNextMillis(long lastTimestamp) {
		long timestamp = System.currentTimeMillis();
		while (timestamp <= lastTimestamp) {
			timestamp = System.currentTimeMillis();
		}
		return timestamp;
	}
}
不知道怎么回事,贴的代码有乱码,完整样例代码请看传送门: https://github.com/tangaiyun/idgen

你可能感兴趣的:(Spring Cloud 微服务架构下的全局id生成器,基于tweet的snowflake算法改进)