分布式唯一ID方案

背景

  在复杂的分布式系统中,往往需要对大量的数据和消息进行唯一标识。

  如对大量的订单做分库分表后,需要有一个唯一的ID来标识一条数据或消息,数据库的自增ID显然不能满足需求。

  

  业务系统对分布式唯一ID的要求:

    ①:全局唯一性,不能重复

    ②:趋势递增,在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDMS使用B-tree的数据结构来存储索引数据,在主键的选择上面应该尽量使用有序的主键保证写入性能

    ③:单调递增,保证下一个ID一定大于上一个ID

    ④:信息安全,如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则

常用方案

 1、UUID

  优点:生成简单,本地生成,没有网络消耗,性能非常高

  缺点:

    1)每次生成的ID是无序的,无法保证趋势递增,不能作为数据库主键,否则会引起索引数据位置频繁变动,严重影响性能

    2)UUID的字符串存储,查询效率慢

    3)存储空间大

    4)ID本身无业务含义,不可读
    

 2、Redis 

  利用redis的原子性自增,具体实现为:

  年份 + 当天距当年第多少天 + 天数 + 小时 + redis自增

  优点:

    有序递增,可读性强
  缺点:

    占用带宽,每次要向redis进行请求

 3、雪花snowflake算法

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

  其核心思想是:使用41bit作为毫秒数,10bit作为work ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

  snowflake算法生成64位的二进制正整数,然后转换成10进制的数。

  64位二进制数组成部分如下:

     

  优点:

    1)此方案每秒能够产生409.6万个ID,性能快

    2)时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增

    3)灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求

  缺点:

    只能保证work id相同的情况下生成的id是递增的

    依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成  

  时钟回拨问题的解决方案:

    1)回拨之后更换work id

    2)等时间追上来再去生成id

  

  Mongo的ObjectId与snowflake类似,12 字节的ObjectId包括:

    一个 4 字节的时间戳,表示 ObjectId 的创建,以 Unix 纪元以来的秒数为单位。
    每个进程生成一次的 5 字节随机值。这个随机值对于机器和过程是唯一的。
    一个 3 字节递增计数器,初始化为随机值

 4、美团Leaf

 https://tech.meituan.com/2017/04/21/mt-leaf.html

  Snowflake代码:

  1)系统时钟 SystemClock:

import java.sql.Timestamp;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 系统时钟<br>
 * 高并发场景下System.currentTimeMillis()的性能问题的优化
 * System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我还没测试过,有人说是100倍左右)
 * System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道
 * 后台定时更新时钟,JVM退出时,线程自动回收
 *
 * see: http://git.oschina.net/yu120/sequence
 * @author lry,looly
 */
public class SystemClock {

	/** 时钟更新间隔,单位毫秒 */
	private final long period;
	/** 现在时刻的毫秒数 */
	private volatile long now;

	/**
	 * 构造
	 * @param period 时钟更新间隔,单位毫秒
	 */
	public SystemClock(long period) {
		this.period = period;
		this.now = System.currentTimeMillis();
		scheduleClockUpdating();
	}

	/**
	 * 开启计时器线程
	 */
	private void scheduleClockUpdating() {
		ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
			Thread thread = new Thread(runnable, "System Clock");
			thread.setDaemon(true);
			return thread;
		});
		scheduler.scheduleAtFixedRate(() -> now = System.currentTimeMillis(), period, period, TimeUnit.MILLISECONDS);
	}

	/**
	 * @return 当前时间毫秒数
	 */
	private long currentTimeMillis() {
		return now;
	}

	//------------------------------------------------------------------------ static
	/**
	 * 单例
	 * @author Looly
	 *
	 */
	private static class InstanceHolder {
		public static final SystemClock INSTANCE = new SystemClock(1);
	}

	/**
	 * @return 当前时间
	 */
	public static long now() {
		return InstanceHolder.INSTANCE.currentTimeMillis();
	}

	/**
	 * @return 当前时间字符串表现形式
	 */
	public static String nowDate() {
		return new Timestamp(InstanceHolder.INSTANCE.currentTimeMillis()).toString();
	}
}

  Snowflake:

import java.io.Serializable;
import java.util.Date;

/**
 * Twitter的Snowflake 算法
 * 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。
 *
 * snowflake的结构如下(每部分用-分开):
 *
 * 符号位(1bit)- 时间戳相对值(41bit)- 数据中心标志(5bit)- 机器标志(5bit)- 递增序号(12bit)
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
 *
 * 第一位为未使用(符号位表示正数),接下来的41位为毫秒级时间(41位的长度可以使用69年)
 * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)
 * 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
 *
 * 并且可以通过生成的id反推出生成时间,datacenterId和workerId
 *
 * 参考:http://www.cnblogs.com/relucent/p/4955340.html<br>
 * 关于长度是18还是19的问题见:https://blog.csdn.net/unifirst/article/details/80408050
 *
 * @author Looly
 * @since 3.0.1
 */
public class Snowflake implements Serializable {
    private static final long serialVersionUID = 1L;


    /**
     * 默认的起始时间,为Thu, 04 Nov 2010 01:42:54 GMT
     */
    public static long DEFAULT_TWEPOCH = 1288834974657L;
    /**
     * 默认回拨时间,2S
     */
    public static long DEFAULT_TIME_OFFSET = 2000L;

    private static final long WORKER_ID_BITS = 5L;
    // 最大支持机器节点数0~31,一共32个
    @SuppressWarnings({"PointlessBitwiseExpression", "FieldCanBeLocal"})
    private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
    private static final long DATA_CENTER_ID_BITS = 5L;
    // 最大支持数据中心节点数0~31,一共32个
    @SuppressWarnings({"PointlessBitwiseExpression", "FieldCanBeLocal"})
    private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
    // 序列号12位(表示只允许workId的范围为:0-4095)
    private static final long SEQUENCE_BITS = 12L;
    // 机器节点左移12位
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    // 数据中心节点左移17位
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    // 时间毫秒数左移22位
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
    // 序列掩码,用于限定序列最大值不能超过4095
    @SuppressWarnings("FieldCanBeLocal")
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 4095

    private final long twepoch;
    private final long workerId;
    private final long dataCenterId;
    private final boolean useSystemClock;
    // 允许的时钟回拨数
    private final long timeOffset;

    private long sequence = 0L;
    private long lastTimestamp = -1L;


    /**
     * 构造
     *
     * @param workerId     终端ID
     * @param dataCenterId 数据中心ID
     */
    public Snowflake(long workerId, long dataCenterId) {
        this(workerId, dataCenterId, false);
    }

    /**
     * 构造
     *
     * @param workerId         终端ID
     * @param dataCenterId     数据中心ID
     * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
     */
    public Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) {
        this(null, workerId, dataCenterId, isUseSystemClock);
    }

    /**
     * @param epochDate        初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用
     * @param workerId         工作机器节点id
     * @param dataCenterId     数据中心id
     * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
     * @since 5.1.3
     */
    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) {
        this(epochDate, workerId, dataCenterId, isUseSystemClock, DEFAULT_TIME_OFFSET);
    }

    /**
     * @param epochDate        初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用
     * @param workerId         工作机器节点id
     * @param dataCenterId     数据中心id
     * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
     * @param timeOffset 允许时间回拨的毫秒数
     * @since 5.7.3
     */
    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) {
        if (null != epochDate) {
            this.twepoch = epochDate.getTime();
        } else{
            // Thu, 04 Nov 2010 01:42:54 GMT
            this.twepoch = DEFAULT_TWEPOCH;
        }
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %s or less than 0", MAX_WORKER_ID));
        }
        if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %s or less than 0", MAX_DATA_CENTER_ID));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
        this.useSystemClock = isUseSystemClock;
        this.timeOffset = timeOffset;
    }

    /**
     * 根据Snowflake的ID,获取机器id
     *
     * @param id snowflake算法生成的id
     * @return 所属机器的id
     */
    public long getWorkerId(long id) {
        return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);
    }

    /**
     * 根据Snowflake的ID,获取数据中心id
     *
     * @param id snowflake算法生成的id
     * @return 所属数据中心
     */
    public long getDataCenterId(long id) {
        return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);
    }

    /**
     * 根据Snowflake的ID,获取生成时间
     *
     * @param id snowflake算法生成的id
     * @return 生成的时间
     */
    public long getGenerateDateTime(long id) {
        return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;
    }

    /**
     * 下一个ID
     *
     * @return ID
     */
    public synchronized long nextId() {
        long timestamp = genTime();
        if (timestamp < this.lastTimestamp) {
            if(this.lastTimestamp - timestamp < timeOffset){
                // 容忍指定的回拨,避免NTP校时造成的异常
                timestamp = lastTimestamp;
            } else{
                // 如果服务器时间有问题(时钟后退) 报错。
                throw new IllegalStateException(String.format("Clock moved backwards. Refusing to generate id for %s ms", lastTimestamp - timestamp));
            }
        }

        if (timestamp == this.lastTimestamp) {
            final long sequence = (this.sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
            this.sequence = sequence;
        } else {
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT)
                | (dataCenterId << DATA_CENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

    /**
     * 下一个ID(字符串形式)
     *
     * @return ID 字符串形式
     */
    public String nextIdStr() {
        return Long.toString(nextId());
    }

    // ------------------------------------------------------------------------------------------------------------------------------------ Private method start

    /**
     * 循环等待下一个时间
     *
     * @param lastTimestamp 上次记录的时间
     * @return 下一个时间
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = genTime();
        // 循环直到操作系统时间戳变化
        while (timestamp == lastTimestamp) {
            timestamp = genTime();
        }
        if (timestamp < lastTimestamp) {
            // 如果发现新的时间戳比上次记录的时间戳数值小,说明操作系统时间发生了倒退,报错
            throw new IllegalStateException(
                    String.format("Clock moved backwards. Refusing to generate id for %s ms", lastTimestamp - timestamp));
        }
        return timestamp;
    }

    /**
     * 生成时间戳
     *
     * @return 时间戳
     */
    private long genTime() {
        return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
    }
    // ------------------------------------------------------------------------------------------------------------------------------------ Private method end
}
  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java分布式唯一ID生成方案有很多种,其中一种比较常用的方案是基于Snowflake算法的ID生成方案。 Snowflake算法是一种基于时间序列生成唯一ID的算法,它可以在分布式系统中生成唯一的、有序的、趋势递增的ID。Snowflake算法生成的ID是一个64位的整数,它的结构如下: ``` 0 - 41位时间戳 - 10位机器标识 - 12位序列号 ``` 其中,时间戳占用了41位,可以表示2^41-1个数字,大约可以支持生成69年的ID;机器标识占用了10位,可以表示1023个不同的机器;序列号占用了12位,可以表示4095个不同的序列号。 Snowflake算法的实现比较简单,可以使用Java的AtomicLong类来实现序列号的自增。具体的实现可以参考下面的代码: ```java public class SnowflakeIdGenerator { private static final long START_TIMESTAMP = 1577808000000L; // 2020-01-01 00:00:00 private static final long MACHINE_ID_BITS = 10L; private static final long SEQUENCE_BITS = 12L; private static final long MACHINE_ID_OFFSET = SEQUENCE_BITS; private static final long TIMESTAMP_OFFSET = MACHINE_ID_BITS + SEQUENCE_BITS; private static final long MAX_MACHINE_ID = (1L << MACHINE_ID_BITS) - 1L; private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1L; private static long machineId = 0L; private static long sequence = 0L; private static long lastTimestamp = -1L; static { String machineName = ""; try { machineName = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { e.printStackTrace(); } machineId = Math.abs(machineName.hashCode()) % MAX_MACHINE_ID; } public synchronized static long nextId() { long currentTimestamp = System.currentTimeMillis(); if (currentTimestamp < lastTimestamp) { throw new IllegalStateException("Clock moved backwards. Refusing to generate id"); } if (currentTimestamp == lastTimestamp) { sequence = (sequence + 1L) & MAX_SEQUENCE; if (sequence == 0L) { currentTimestamp = waitUntilNextMillis(currentTimestamp); } } else { sequence = 0L; } lastTimestamp = currentTimestamp; return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_OFFSET) | (machineId << MACHINE_ID_OFFSET) | sequence; } private static long waitUntilNextMillis(long currentTimestamp) { while (currentTimestamp == lastTimestamp) { currentTimestamp = System.currentTimeMillis(); } return currentTimestamp; } } ``` 使用这个类可以生成全局唯一ID,可以在分布式系统中使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值