雪花算法基本情况

  • 雪花算法是一个分布式的唯一ID生成器。
  • 它应该具有高并发,以及高性能优点。
  • 基于时间戳,ID具有有序性,同时分布式下机器间时间差异过大(类似同一台机器时间回拨,一定会重复),会导致重复ID。
  • 基于机器码和操作中心id,ID具有不可重复性。
  • 它的ID是8字节64bit的一个Long长整型数据。

ID基本组成

ID基本组成:
不用: 1bit,因为最高位是符号位,0表示正,1表示负,所以这里固定为0
时间戳: 41bit,服务上线的时间毫秒级的时间戳(为当前时间-服务第一次上线时间),这里为(2^41-1)/1000/60/60/24/365 = 49.7年
工作机器id: 10bit,表示工作机器id,用于处理分布式部署id不重复问题,可支持2^10 = 1024个节点
序列号: 12bit,用于离散同一机器同一毫秒级别生成多条Id时,可允许同一毫秒生成2^12 = 4096个Id,则一秒就可生成4096*1000 = 400w个Id

iOS 生成设备的唯一id 唯一id生成器算法_分布式


这样看,在分布式时,通过时间戳+工作机器ID+散列序列号,几乎不会重复,当然你必须保证工作机器ID的准确配置。

源码简易分析以及注意事项

我们以mybatisplus实现的雪花算法为例:

  • IdWorker类:
    注意:DefaultIdentifierGenerator()这个必须是单例模式的,否者在多线程高并发下,会有重复ID,而且很多。详见IDENTIFIER_GENERATOR.nextId(entity)这个方法实现
package com.baomidou.mybatisplus.core.toolkit;

import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;

public class IdWorker {
	/**单例模式,否者会多个重复Id,
	详见IDENTIFIER_GENERATOR.nextId(entity).longValue()*/
    private static IdentifierGenerator IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator();
    public static final DateTimeFormatter MILLISECOND = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");

    public IdWorker() {
    }
	/**获取Id*/
    public static long getId() {
        return getId(new Object());
    }
	/**获取Id,最终都会走这一步*/
    public static long getId(Object entity) {
        return IDENTIFIER_GENERATOR.nextId(entity).longValue();
    }

	/**机器码和数据中心的ID,参数不能重复*/
    public static void initSequence(long workerId, long dataCenterId) {
        IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator(workerId, dataCenterId);
    }
}
  • IDENTIFIER_GENERATOR.nextId(entity).longValue() 方法
    nextId是一个线程安全的方法,这也是多线程唯一Id生成的必要之一。
    synchronized并不会直接转成重量级锁,因为java在1.6已经优化,因此在资源上没有过分占用,是从偏向锁,轻量级锁,再到重量级锁的过程。
public synchronized long nextId() {
		 /** long timestamp = this.timeGen();这步代码一定要注意当你DefaultIdentifierGenerator是多例时,这里获取的timestamp不具有真正的唯一性,因为多个实例一起在工作,所以写工具类时不要写成多例 */
        long timestamp = this.timeGen();

		// 这里是时间异常,针对分布式下,不同机器时间的差异过大
        if (timestamp < this.lastTimestamp) {
            long offset = this.lastTimestamp - timestamp;
            if (offset > 5L) {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
            }

            try {
                this.wait(offset << 1);
                timestamp = this.timeGen();
                if (timestamp < this.lastTimestamp) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            } catch (Exception var6) {
                throw new RuntimeException(var6);
            }
        }

        if (this.lastTimestamp == timestamp) {
            this.sequence = this.sequence + 1L & 4095L;
            if (this.sequence == 0L) {
            	// 散列序列号没了,必须转到下一毫秒,采用阻塞
                timestamp = this.tilNextMillis(this.lastTimestamp);
            }
        } else {
       		//  散列序列号
            this.sequence = ThreadLocalRandom.current().nextLong(1L, 3L);
        }

        this.lastTimestamp = timestamp;
        // 或运算连接运算后的时间戳,数据中心,工作中心,序列号的bit位,会被自动返回为一个长整型的数据。
        return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence;
    }

雪花算法很好用,让我了解了更多的东西,学习了。