分布式环境,生成全局唯一的订单号 SnowFlake SnowFlake算法实现全局唯一的订单号

在探究订单号唯一的过程中,我们不得不先了解Twitter SnowFlake(雪花算法),github上可查看器源码,下载地址https://github.com/twitter/snowflake,源码使用Scala进行编写。接下来我们先来了解一下SnowFlake。

分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。

有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。

而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务。

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。

这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等。

其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。

比如在snowflake中的64-bit分别表示如下图(图片来自网络)所示:
 

分布式环境,生成全局唯一的订单号
SnowFlake
SnowFlake算法实现全局唯一的订单号

 整个结构是64位,所以我们在Java中可以使用long来进行存储。 该算法实现基本就是二进制操作,单机每秒内理论上最多可以生成1024*(2^12),也就是409.6万个ID(1024 X 4096 = 4194304)


0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 

  •   1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
  •   41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
  •   10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId。10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。
  •  12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

 加起来刚好64位,为一个Long型。


优点:

  • 整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
  • 针对此,美团做出了改进:https://github.com/Meituan-Dianping/Leaf

源码解析:

nextId方法:代码中sequence = (sequence + 1) & sequenceMask;语句作用主要是防止sequence的值超过最大序列4095,

其中使用了按位与运算符(&),(&)的运算规则:0&0=0;  0&1=0;   1&0=0;    1&1=1;   比如sequenceMask最大值是3,那么3转换成二进制是  1  1  ,而1的二进制是 0  1 ,1 & 3的结果是  1,依次可以得出 2 & 3的结果是  2、 3 & 3的结果是  3、 4 & 3的结果是  0(当然sequenceMask必须是奇数才可以这样使用,否则当超过最大值的时候无法等于0);

nextId是线程安全的方法,因此当sequence的值等于0时将等待到下一毫秒。

接下来我们看看返回值

((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)  | (workerId << workerIdShift) | sequence

先看第一部分(timestamp - twepoch) << timestampLeftShift):

twepoch开始的时间戳,这个时间我们可以自己设置,可以是系统开始部署的时间,也可以比这个时间更早;

timestamp是生成id的时间,第一部分即可获取到  当前时间戳-开始时间戳;

timestampLeftShirft = sequenceBits + workerIdBits + datacenterIdBits; (timestampLeftShirft  = 序列的位数12位 + 机器id的位数5位 + 数据表示的位数5位),因此timestampLeftShirft的值是22;

接着看位运算  当前时间戳 - 开始时间戳 最终向左位移22位,也就是相当于 时间戳的差 左边补了22位0,

接下来的运算与之对应 (datacenterId << datacenterIdShift)数据表示向左移动了17=(sequenceBits + workerIdBits)位,(workerId << workerIdShift)机器id向左移动了12=(sequenceBits)位,然后各自结果进行|运算。

(位或运算规则:0|0=0;  0|1=1;  1|0=1;   1|1=1;)

分布式环境,生成全局唯一的订单号
SnowFlake
SnowFlake算法实现全局唯一的订单号

/**
 * Twitter_Snowflake<br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * 加起来刚好64位,为一个Long型。<br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 */
public class SnowflakeIdWorker {

    // ==============================Fields===========================================
    /** 开始时间截 (2015-01-01) */
    private final long twepoch = 1420041600000L;

    /** 机器id所占的位数 */
    private final long workerIdBits = 5L;

    /** 数据标识id所占的位数 */
    private final long datacenterIdBits = 5L;

    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 支持的最大数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 序列在id中占的位数 */
    private final long sequenceBits = 12L;

    /** 机器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 数据标识id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 时间截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作机器ID(0~31) */
    private long workerId;

    /** 数据中心ID(0~31) */
    private long datacenterId;

    /** 毫秒内序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的时间截 */
    private long lastTimestamp = -1L;

    //==============================Constructors=====================================
    /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    // ==============================Methods==========================================
    /**
     * 获得下一个ID (该方法是线程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (datacenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    //==============================Test=============================================
    /** 测试 */
    public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
        for (int i = 0; i < 1000; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
        }
    }
}

SnowFlake算法实现全局唯一的订单号

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Title 订单号生成
 * @Description
 *  订单号组成: 时间戳(yyMMddHHmmssSSS) + 机器ID + 机构ID + 序号</br>
 *  时间戳  15位</br>
 *  机器ID 2位</br>
 *  序号 4位</br>
 *  机构ID 4位
 *  订单号总长度25位,
 * @author liuzhimin
 * 
 */
public class OrderUtils {

	/** 序列 */
    private final long sequenceBits = 12L;
    
    // 最近的时间戳
    private long lastTimestamp=0;
    
    // 0,并发控制
    private long sequence = 0L;
    
    private String machineId;
    
    private String organizerId;
    
    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    
    public OrderUtils(int machineId,int organizerId) {
    	this.machineId = leftPad(machineId,2); 
    	this.organizerId = leftPad(organizerId,4); 
    }
    
    /**
     * 生成订单号
     */
    public synchronized String nextId(){
    	Date now=new Date();
    	SimpleDateFormat formatter = new SimpleDateFormat("yyMMddHHmmssSSS");
        String time= formatter.format(now);
        long timestamp = now.getTime();
    	 
        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
        
      //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
            	now = tilNextMillis(lastTimestamp);
            	time= formatter.format(now);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }
 
        //上次生成ID的时间截
        lastTimestamp = now.getTime();
        
        String seq = leftPad(sequence, 4);
        StringBuilder sb=new StringBuilder(time).append(machineId).append(organizerId).append(seq);
        return sb.toString();
    }
    
    /**
     * 补码
     * @param i
     * @param n
     * @return
     */
    private String leftPad(long i,int n){
        String s = String.valueOf(i);
        StringBuilder sb=new StringBuilder();
        int c= n - s.length();
        c=c<0?0:c;
        for (int t=0;t<c;t++){
            sb.append("0");
        }
        return sb.append(s).toString();
    }
    
    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected Date tilNextMillis(long lastTimestamp) {
        Date now=new Date();
        long timestamp = now.getTime();
        while (timestamp <= lastTimestamp) {
        	now = new Date();
        	timestamp = now.getTime();
        }
        return now;
    }
    
}