您现在的位置:首页 >> 基础 >> Java编程 >> 内容

Java项目实践,分布式系统如何生成ID,重点介绍雪花算法

时间:2020-12-28 9:15:06

  核心提示:在分布式系统中,如何在各个不同的服务器上产生数据主键ID值?比如,有一个订单系统被部署在了AB两个节点上(即两台服务器上),那么如何在这两个节点上各自生成订单ID,并且保证ID值不会冲突? 通常有以下...

在分布式系统中,如何在各个不同的服务器上产生数据主键ID值?

比如,有一个订单系统被部署在了AB两个节点上(即两台服务器上),那么如何在这两个节点上各自生成订单ID,并且保证ID值不会冲突?

通常有以下3种方法:

1、使用数据库的自增特性(或Oracle中的序列) , 不同节点直接使用相同数据库的自增ID值。

2、使用UUID算法产生ID值。

3、使用SnowFlake算法产生ID值。

由于Java提供了对UUID的支持, 可以直接通过UUID.random UUID) 获取到UUID值。

本文重点介绍SnowFlake算法。SnowFlake算法被称为雪花算法, 是由Twitter提出的一种生成ID的算法, 该算法会生成一个64bit的整数,共可以表示263个ID值。

SnowFlake算法采用“41位时间戳+10位机器码+12位序列号”的大致思想是, 当多个节需要生成多个ID值时,先判断这些节点是否是在同一时刻(精确到ms)生成的ID。

如果不是位序列号”区分ID值。可以直接根据41位时间截区分出ID值;如果是在同一时刻生成的,就再根据“10位机器码和12位序列号“来区分ID值。

具体算法代码:

//本例将10位机器码看成是“5位datacenterId+5位workerId”
public class SnowFlake {

	   private long workerId;
	    private long datacenterId;
	    //每毫秒生产的序列号之从0开始递增;
	    private long sequence = 0L;
	    /*
	        1288834974657L是1970-01-01 00:00:00到2010年11月04日01:42:54所经过的毫秒数;
	        因为现在二十一世纪的某一时刻减去1288834974657L的值,正好在2^41内。
	        因此1288834974657L实际上就是为了让时间戳正好在2^41内而凑出来的。
	        简言之,1288834974657L(即1970-01-01 00:00:00),就是在计算时间戳时用到的“起始时间”。
	     */
	    private long twepoch = 1288834974657L;

	    private long workerIdBits = 5L;
	    private long datacenterIdBits = 5L;
	    private long maxWorkerId = -1L ^ (-1L <<workerIdBits);
	    private long maxDatacenterId = -1L ^ (-1L <<datacenterIdBits);
	    private long sequenceBits = 12L;

	    private long workerIdShift = sequenceBits;
	    private long datacenterIdShift = sequenceBits + workerIdBits;
	    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
	    private long sequenceMask = -1L ^ (-1L <<sequenceBits);

	    private long lastTimestamp = -1L;

	    public SnowFlake(long datacenterId, long workerId) {
	        if ((datacenterId >maxDatacenterId || datacenterId <0)
	                ||(workerId >maxWorkerId || workerId <0)) {
	            throw new IllegalArgumentException("datacenterId/workerId值非法");
	        }
	        this.datacenterId = datacenterId;
	        this.workerId = workerId;
	    }

	    //通过SnowFlake生成id的核心算法
	    public synchronized long nextId() {
	        //获取计算id时刻的时间戳
	        long timestamp = System.currentTimeMillis();

	        if (timestamp <lastTimestamp) {
	            throw new RuntimeException("时间戳值非法");
	        }
	        //如果此次生成id的时间戳,与上次的时间戳相同,就通过机器码和序列号区分id值(机器码已通过构造方法传入)
	        if (lastTimestamp == timestamp) {
	            /*
	                下一条语句的作用是:通过位运算保证sequence不会超出序列号所能容纳的最大值。
	                例如,本程序产生的12位sequence值依次是:1、2、3、4、...、4094、4095
	                (4095是2的12次方的最大值,也是本sequence的最大值)
	                那么此时如果再增加一个sequence值(即sequence + 1),下条语句就会
	                使sequence恢复到0。
	                即如果sequence==0,就表示sequence已满。
	             */
	            sequence = (sequence + 1) &sequenceMask;
	 //如果sequence已满,就无法再通过sequence区分id值;因此需要切换到下一个时间戳重新计算。
	            if (sequence == 0) {
	                timestamp = tilNextMillis(lastTimestamp);
	            }
	        } else {
	            //如果此次生成id的时间戳,与上次的时间戳不同,就已经可以根据时间戳区分id值
	            sequence = 0L;
	        }
	        //更新最近一次生成id的时间戳
	        lastTimestamp = timestamp;
	        /*
	            假设此刻的值是(二进制表示):
	                41位时间戳的值是:00101011110101011101011101010101111101011
	                5位datacenterId(机器码的前5位)的值是:01101
	                5位workerId(机器码的后5位)的值是:11001
	                sequence的值是:01001
	            那么最终生成的id值,就需要:
	                1.将41位时间戳左移动22位(即移动到snowflake值中时间戳应该出现的位置);
	                2.将5位datacenterId向左移动17位,并将5位workerId向左移动12位
	                (即移动到snowflake值中机器码应该出现的位置);
	                3.sequence本来就在最低位,因此不需要移动。
	            以下<<和|运算,实际就是将时间戳、机器码和序列号移动到snowflake中相应的位置。
	         */
	        return ((timestamp - twepoch) <<timestampLeftShift)
	                | (datacenterId <<datacenterIdShift) | (workerId <<workerIdShift)
	                | sequence;
	    }

	    protected long tilNextMillis(long lastTimestamp) {
	        long timestamp = System.currentTimeMillis();
	        /*
	            如果当前时刻的时间戳<=上一次生成id的时间戳,就重新生成当前时间。
	            即确保当前时刻的时间戳,与上一次的时间戳不会重复。
	         */
	        while (timestamp <= lastTimestamp) {
	            timestamp = System.currentTimeMillis();
	        }
	        return timestamp;
	    }
}

测试算法代码:

public class TestSnowFlake {
	//测试1秒能够生成的id个数
		public static void generateIdsInOneSecond() {
			 SnowFlake idWorker = new SnowFlake(1, 1);
			 long start = System.currentTimeMillis();
			 int i = 0;
			 for (; System.currentTimeMillis() - start <1000; i++) {
					// idWorker.nextId();
					 System.out.println(idWorker.nextId());
			 }
			 long end = System.currentTimeMillis();
			 System.out.println("耗时:"+ (end - start));
			 System.out.println("生成id个数:"+ i);
		}

		public static void main(String[] args) {
			 generateIdsInOneSecond();
	}
}

测试结果图:

我是一名码龄10年的程序员,在这里会分享实在干货,让你少走弯路,成就精彩人生。

Java免费学习   Java自学网 http://www.javalearns.com

关注微信号:javalearns   随时随地学Java

或扫一扫

随时随地学Java

作者:不详 来源:网络
    你是从哪里知道本网站的?
  • 网友介绍的
  • 百度搜索的
  • Google搜索的
  • 其它搜索过来的
  • 网址输错了进来的
  • 太忙了不记得了
共有评论 0相关评论
发表我的评论
  • 大名:
  • 内容:
  • java学习网(www.javalearns.com) © 2014 版权所有 All Rights Reserved.
  • Email:javalearns@163.com 站长QQ:1356121699 晋ICP备14003680号-3
  • java学习网部分内容来自网络或网友发布,如侵犯了您利益,请发邮件至:javalearns@126.com,我们尽快处理!
  • Java学习网
  • 网站统计
  • 晋公网安备 14042902000001号