Java唯一序列订单号生成源码,基于snowflake算法

Snowflake简介

Snowflake生成的每一个ID都是64位的整型数,它的核心算法也比较简单高效,结构如下:
  • 41位的时间序列,精确到毫秒级,41位长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。

  • 10位的机器标识,10位的长度最多支持部署1024个节点。

  • 12位的计数序列号,序列号即一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。

  • 最高位是符号位,始终为0,不可用。

Snowflake原生算法java实现

package com.app.xm.utils;

/**
 * 唯一序列ID,原生java算法
 * 从网上查找的源码
 * @author ouyangjun
 *
 */
public class IdSequenceUtils {
	
	private long workerId;
	private long datacenterId;
	private long sequence = 0L;
	private long twepoch = 1514736000000L; //从该时间2018-01-01 00:00:00开始
	private long workerIdBits = 5L; //节点ID长度
	private long datacenterIdBits = 5L; //数据中心ID长度
	private long maxWorkerId = -1L ^ (-1L << workerIdBits); //最大支持机器节点数0~31,一共32个
	private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); //最大支持数据中心节点数0~31,一共32个
	private long sequenceBits = 12L; //序列号12位
	private long workerIdShift = sequenceBits; //机器节点左移12位
	private long datacenterIdShift = sequenceBits + workerIdBits; //数据中心节点左移17位
	private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; //时间毫秒数左移22位
	private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095
	private long lastTimestamp = -1L;
	
	public IdSequenceUtils() {
		this(0L, 0L);
	}
	
	public IdSequenceUtils(long workerId, long datacenterId) {
		if (workerId > maxWorkerId || workerId < 0) {
			throw new IllegalArgumentException(String.format("业务ID不能大于%d或小于0", maxWorkerId));
		}
	
		if (datacenterId > maxDatacenterId || datacenterId < 0) {
			throw new IllegalArgumentException(String.format("数据中心ID不能大于%d或小于0", maxDatacenterId));
		}
		this.workerId = workerId;
		this.datacenterId = datacenterId;
	}
	
	/**
	 * 调用该方法,获取序列ID
	 * @return
	 */
	public synchronized long nextId() {
		long timestamp = currentTime(); //获取当前时间毫秒数
		//如果服务器时间有问题(时钟后退) 报错
		if (timestamp < lastTimestamp) {
			throw new RuntimeException(String.format("时钟向后移动。拒绝在%d毫秒内生成id", lastTimestamp - timestamp)); 
		}
	
		//如果上次生成时间和当前时间相同,在同一毫秒内
		if (lastTimestamp == timestamp) {
			//sequence自增,因为sequence只有12bit,所以和sequenceMask相与一下,去掉高位
			sequence = (sequence + 1) & sequenceMask;
			//判断是否溢出,也就是每毫秒内超过4095,当为4096时,与sequenceMask相与,sequence就等于0
			if (sequence == 0) {
				timestamp = nextMillis(lastTimestamp); //自旋等待到下一毫秒
			}
		} else {
			sequence = 0L; //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加
		}
		lastTimestamp = timestamp;
		// 最后按照规则拼出ID。
		// 000000000000000000000000000000000000000000 00000 00000 000000000000
		// time datacenterId workerId sequence
		return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
				| (workerId << workerIdShift) | sequence;
	}
	
	/**
	 * 比较当前时间戳和下一个时间戳,如果下一个时间戳等于或小于当前时间戳,则循环获取下个时间戳
	 * 该方法主要是避免同一时间获取同一时间戳
	 * @param lastTimestamp
	 * @return
	 */
	protected long nextMillis(long lastTimestamp) {
		long timestamp = currentTime();
		while (timestamp <= lastTimestamp) {
			timestamp = currentTime();
		}
		return timestamp;
	}
	
	/**
	 * 获取系统当前时间戳
	 * @return
	 */
	protected long currentTime() {
		return System.currentTimeMillis();
	}
	
}

snowflake唯一序列订单号生成源码

package com.app.xm.utils;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;

/**
 * 订购业务唯一订单号实现
 * @author ouyangjun
 *
 */
public class IdGeneratorUtils {
	
	private long workerId;   //用ip地址最后几个字节标示
	private long datacenterId = 0L; //可配置在properties中,启动时加载,此处默认先写成0
	private long sequence = 0L;
	private long workerIdBits = 8L; //节点ID长度
	//private long datacenterIdBits = 2L; //数据中心ID长度,可根据时间情况设定位数
	private long sequenceBits = 12L; //序列号12位
	private long workerIdShift = sequenceBits; //机器节点左移12位
	private long datacenterIdShift = sequenceBits + workerIdBits; //数据中心节点左移17位
	private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095
	private long lastTimestamp = -1L;

	public IdGeneratorUtils() {
	    workerId = 0x000000FF & getLastIP();
	}

	/**
	 * 调用该方法,获取序列ID
	 * @return
	 */
	public synchronized String nextId() {
	    long timestamp = currentTime(); //获取当前毫秒数
	    //如果服务器时间有问题(时钟后退) 报错。
	    if (timestamp < lastTimestamp) {
	        throw new RuntimeException(String.format("时钟向后移动。拒绝在%d毫秒内生成id", lastTimestamp - timestamp));
	    }
	    //如果上次生成时间和当前时间相同,在同一毫秒内
	    if (lastTimestamp == timestamp) {
	        //sequence自增,因为sequence只有12bit,所以和sequenceMask相与一下,去掉高位
	        sequence = (sequence + 1) & sequenceMask;
	        //判断是否溢出,也就是每毫秒内超过4095,当为4096时,与sequenceMask相与,sequence就等于0
	        if (sequence == 0) {
	            timestamp = nextMillis(lastTimestamp); //自旋等待到下一毫秒
	        }
	    } else {
	        sequence = 0L; //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加
	    }
	    lastTimestamp = timestamp;

	    long suffix = (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;

	    // 格式化日期
	    SimpleDateFormat timePe = new SimpleDateFormat("yyyyMMddHHMMssSSS");
	    String datePrefix = timePe.format(timestamp);

	    return datePrefix + suffix;
	}

	/**
	 * 比较当前时间戳和下一个时间戳,如果下一个时间戳等于或小于当前时间戳,则循环获取下个时间戳
	 * 该方法主要是避免同一时间获取同一时间戳
	 * @param lastTimestamp
	 * @return
	 */
	protected long nextMillis(long lastTimestamp) {
		long timestamp = currentTime();
		while (timestamp <= lastTimestamp) {
			timestamp = currentTime();
		}
		return timestamp;
	}

	/**
	 * 获取系统当前时间戳
	 * @return
	 */
	protected long currentTime() {
		return System.currentTimeMillis();
	}

	/**
	 * 获取当前本地IP
	 * @return
	 */
	private byte getLastIP(){
	    byte lastip = 0;
	    try{
	        InetAddress ip = InetAddress.getLocalHost();
	        byte[] ipByte = ip.getAddress();
	        lastip = ipByte[ipByte.length - 1];
	    } catch (UnknownHostException e) {
	        e.printStackTrace();
	    }
	    return lastip;
	}
	
}

多线程测试获取唯一序列订单号:

package com.app.xm.test;

import com.app.xm.utils.IdGeneratorUtils;

public class TestSequence extends Thread {
	
	IdGeneratorUtils idGenerator;
	
	public TestSequence(IdGeneratorUtils idGenerator) {
            this.idGenerator = idGenerator;
        }
	
	int ct = 0;
	
	@Override
	public void run() {
		System.out.println();
		while(ct<100) {
			System.out.println(Thread.currentThread().getId() + " : " + idGenerator.nextId());
			ct++;
		}
	}

	
	public static void main(String[] args) {
		
		IdGeneratorUtils utils = new IdGeneratorUtils();
		
		TestSequence test1 = new TestSequence(utils);
		TestSequence test2 = new TestSequence(utils);
		
		test1.start();
		test2.start();
	}

}

其它方式实现序列唯一:

方式一:JAVA原生API提供的UUID,它能确保唯一,但是有个缺点,UUID是一个字符串,字母数字组合,对于分布式系统中数据库主键的生成就不行了,由于它是本地生成,没有远程调用,不存在网络时间,效率比较高,在主键非整数时可以使用

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


展开阅读全文

没有更多推荐了,返回首页