雪花算法的实现原理

雪花算法是由Twitter开源的分布式ID生成算法,主要应用于分库分表场景中的全局ID作为业务主键,或者生成全局唯一的订单号的场景。
为什么要叫雪花算法呢?相关研究表明,一般的雪花大约由10^{19}个水分子组成。在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己独特的形状。雪花算法的意思是生成的ID如雪花般独一无二。
其实,单单解决唯一ID这个问题有很多解决方法,比如UUID、系统时间戳、Redis原子递增、数据库全局表自增ID等。但是在实际应用中,ID除了需要满足唯一性,还需要满足以下特征:
1) 单调递增:保证下一个ID号一定大于上一个。
2) 保证安全:ID号需要满足无规则性,不能让别人根据ID号猜出我们的信息和业务数据量,增加恶意用户扒取数据的难度。
3) 含时间戳:需要记录系统时间戳。
4) 高可用:发布一个获取分布式ID的请求,服务器至少要保证99.999%的情况下可以创建一个全局唯一的分布式ID。
5) 低延迟:发布一个获取分布式ID的请求要快(急速)。
6) 高 QPS: 假如 10万个创建分布式ID的请求同时“杀过来”,服务器要“顶得住”并且成功创建10万个分布式ID。
雪花算法就是一个比较符合这类特征的全局唯一算法。在很多大厂的全局ID组件中,都用到了雪花算法,比如百度的UidGenerator,美团的Leaf算法等。

雪花算法生成的是一个由64个bit(比特)位组成的long类型的数字。如下图所示,它分为4个部分。


第1部分,用1个bit(比特)位来表示1个符号位,因为ID一般不会是负数,所以一般情况下就是0.
第2部分,用41个bit(比特)位来表示系统时间戳,这个时间戳就是系统时间记录的毫秒数。41个bit可以表示的最大数字为2^{41} -1毫秒,换算成时间为69年。
第3部分,用10个bit(比特)位来表示工作机器的ID,用来保证多个服务器上生成D的唯一性。如果存在跨机房部署的情况,那么还可以把这10个比特位拆分为两组,每组5个 bit(比特)位。前面的5个bit(比特)表示机房ID,后面5个bit(比特)表示机器ID。10个比特位的最大值是2^{10},也就是最多1024台机器。
第4部分,用12个bit(比特)位来表示递增序列,用来记录同一毫秒内产生不同ID的能力。它的最大值为2^{12}-1,也就是4096。
雪花算法就是根据这四个部分的组成规则,生成对应bit位的数据,然后组装到一起生成一个全局唯一ID.
雪花算法主要有以下优点:
1) 分布式系统内不会产生ID碰撞,效率高。
2) 不需要依赖数据库等第三方系统,稳定性更高,可以根据自身业务分配bit位,非常灵活。
3) 生成ID的性能也非常高,每秒能生成26万个自增可排序的ID。
当然,雪花算法也有缺点,因为它依赖机器时钟,如果机器时钟回拨,则可能导致ID重复。在分布式环境下,每台机器上的时钟不可能完全同步,有时会出现不是全局递增的情况。当然大部分情况下可以忽略这一缺点。

下面使用java代码实现一个雪花算法:




/**
 * 雪花算法
 */
public class SnowFlake {
    //开始时间戳
    // 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
    // 2024-02-20 15:21:35
    private final long twepoch = 1708413660021L;

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

    //数据标识Id所占的位数
    private final long dataCenterIdBits = 5L;

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

    //支持的最大数据标识Id,结果是31
    private final long maxdataCenterId = ~(-1L << dataCenterIdBits);

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

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

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

    //时间戳向左移22位(5 + 5 + 12)
    private final long timestampleLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;

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

    //工作机器Id(0~31)
    private volatile long workerId;

    //数据中心ID(0~31)
    private volatile long dataCenterId;

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

    //上次生成ID的时间戳
    private volatile long lastTimestamap = -1L;


    /**
     * 构造函数
     * @param workerId  工作Id(0~31)
     * @param dataCenterId 数据中心ID(0~31)
     */

    public SnowFlake(long workerId, long dataCenterId) throws IllegalAccessException {
        if (workerId > maxWorkerId || workerId < 0){
            throw  new IllegalAccessException(
                    String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)
            );
        }

        if (dataCenterId > maxdataCenterId || dataCenterId < 0){
            throw new IllegalAccessException(
                    String.format("dataCenter Id can't be greater than %d or less than 0", maxdataCenterId)
            );
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }

    /**
     * 获取下一个Id(该方法是线程安全的)
     * 如果一个线程反复获取synchronized锁,那么synchronized锁将变成偏向锁
     *
     */
    public synchronized long nextId(){
        long timestamp = System.currentTimeMillis();

        //如果当前时间小于上一次ID生成的时间戳,则说明系统时钟回退过,这个时候应该抛异常
        if (timestamp < lastTimestamap){
            throw new RuntimeException(
                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamap - timestamp)
            );
        }
        //如果是同一时刻生成的,则进行毫秒内序列
        if (lastTimestamap == timestamp){
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0){
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamap);
            }

        }

        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成Id的时间戳
        lastTimestamap = timestamp;

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


    }


    /**
     * 阻塞到下一个毫秒,直到获取新的时间戳
     * @param lastTimestamap
     * @return
     */
    private long tilNextMillis(long lastTimestamap){
        long timestamp = System.currentTimeMillis();
        while (timestamp <=lastTimestamap){
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
    
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江湖中的阿龙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值