【JAVA】系统唯一ID生成方案讨论

这种文章,网上应该很多了,不过自己不写一遍,总是不会印象太深刻,所以今天为了再度加深印象,自己也写一遍。

现在的互联网项目,用户数越来越多,系统基本都是分布式部署,所以基于数据库的自增id这里就不说了。

1、UUID

这个东西是JAVA原生API提供的,它的确能保证唯一,但是有个弊端,它是一个字符串,字母数字组合,对于分布式系统中数据库主键的生成就不行了,数据库主键大家都是用整型类型来定义的,字母数字组合这种肯定不行了。

但是非数据库主键生成的场景倒是可以用用,必须消息唯一性这种,它是本地生成,没有远程调用,不存在网络时间,效率高;如果用它来建索引,效率是很低的。


2、独立的ID生成服务

这个应该比较多见,专门搭建一个系统用来给各个接入系统分配唯一ID,每个系统每次来请求的时候返回一段ID,系统拿到自己用,用完后,再来申请,再次分配下一区段的,以此类推。

这种方法,如果ID生成服务出现故障,那对其它所有系统来说都是灾难,可靠性要求太高了,性能效率方面倒是没有什么问题,区间分配,效率很高。


3、时间戳

直接取当前毫秒时间戳,效率高,也是整型数字,但是对并发量要求高的就不行了,无法保证唯一,1秒最多只能生成1000个嘛,因为是毫秒时间戳,小规模系统可以用用的,简单高效。


4、snowflake算法

这是twitter的一个id生成算法

Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同。

首先我们需要一个long类型的变量来保存这个生成的id,第一位固定为0,因为id都是正数嘛,还剩63位,用x位表示毫秒时间戳,用y位表示进程id,用z位表示同一个时间戳下的序列号,x+y+z=63。我们直接看代码

package id.test;

import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.Set;

/**
 * ClassName:IdGenerator <br/>
 * Function: TODO ADD FUNCTION. <br/>
 * Reason: TODO ADD REASON. <br/>
 * Date: 2017年2月17日 下午3:08:34 <br/>
 * 
 * @author chiwei
 * @version
 * @since JDK 1.6
 * @see
 */
public class IdGenerator {
	/**
	 * SnowFlake算法 64位Long类型生成唯一ID 第一位0,表明正数 2-42,41位,表示毫秒时间戳差值,起始值自定义
	 * 43-52,10位,机器编号,5位数据中心编号,5位进程编号 53-64,12位,毫秒内计数器 本机内存生成,性能高
	 * 
	 * 主要就是三部分: 时间戳,进程id,序列号 时间戳41,id10位,序列号12位
	 * 
	 * @author chiwei
	 * @param args
	 * @since JDK 1.6
	 */

	private final static long beginTs = 1483200000000L;

	private long lastTs = 0L;

	private long processId;
	private int processIdBits = 10;

	private long sequence = 0L;
	private int sequenceBits = 12;

	// 10位进程ID标识
	public IdGenerator(long processId) {
		if (processId > ((1 << processIdBits) - 1)) {
			throw new RuntimeException("进程ID超出范围,设置位数" + processIdBits + ",最大"
					+ ((1 << processIdBits) - 1));
		}
		this.processId = processId;
	}

	protected long timeGen() {
		return System.currentTimeMillis();
	}

	public synchronized long nextId() {
		long ts = timeGen();
		if (ts < lastTs) {// 刚刚生成的时间戳比上次的时间戳还小,出错
			throw new RuntimeException("时间戳顺序错误");
		}
		if (ts == lastTs) {// 刚刚生成的时间戳跟上次的时间戳一样,则需要生成一个sequence序列号
			// sequence循环自增
			sequence = (sequence + 1) & ((1 << sequenceBits) - 1);
			// 如果sequence=0则需要重新生成时间戳
			if (sequence == 0) {
				// 且必须保证时间戳序列往后
				ts = nextTs(lastTs);
			}
		} else {// 如果ts>lastTs,时间戳序列已经不同了,此时可以不必生成sequence了,直接取0
			sequence = 0L;
		}
		lastTs = ts;// 更新lastTs时间戳
		return ((ts - beginTs) << (processIdBits + sequenceBits)) | (processId << sequenceBits)
				| sequence;
	}

	protected long nextTs(long lastTs) {
		long ts = timeGen();
		while (ts <= lastTs) {
			ts = timeGen();
		}
		return ts;
	}

	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		IdGenerator ig = new IdGenerator(1023);
		String str = "20170101";
		System.out.println(new SimpleDateFormat("YYYYMMDD").parse(str).getTime());
		Set<Long> set = new HashSet<Long>();
		long begin = System.nanoTime();
		for (int i = 0; i < 10; i++) {
			set.add(ig.nextId());
		}
		System.out.println("time=" + (System.nanoTime() - begin)/1000.0 + " us");
		System.out.println(set.size());
		System.out.println(set);
	}
}
41的时间戳,存储当前时间戳与开始时间戳的差值,大概可以用69年,当然x,y,z可以自己根据情况分配,不是固定的。

此方法同样是本地生成,效率非常高,唯一性满足度很高,只需要以上一个类就行了,每个进程启动时,分配不同的processId即可。




猜你喜欢

转载自blog.csdn.net/chiweitree/article/details/56276878