lotus

贵有恒何必三更眠五更起 最无益只怕一日曝十日寒

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理
背景

当下绝大部分互联网公司采用的是分布式的架构系统,而分布式系统中有一些场景需要使用到全局性唯一ID,例如:订单编号、付款单编号、交易流水号等等,在这之前,我们可以使用UUID、数据库自增ID等去实现它,但是要么生成的ID是无序的,要么ID生成效率低下。
所以在该背景下,twitter公司提出了snowflake算法,最初Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同,所以twitter开发了这样一套全局唯一ID生成服务。

介绍
snowflake算法生成的64位ID = 1位符号位 + 41位时间戳 + 10位机器id + 12位序列号。


 
64位ID组成结构.png

1、符号位:一般默认为0,表明该ID为正数。

2、时间戳:该时间戳并不是指当前的时间戳,而是当前时间戳-初始时间戳(初始时间戳离当前时间越近越好,而且必须小于当前时间戳)的值。这样做的好处是有更多的数值可以使用,最多可以使用(1L << 41)/ (1000毫秒 * 60秒 * 60 分钟 * 24小时 * 365天) = 69年。

3、机器id:取值在0 ~ 1023之间,最多支持1024个节点。
假设你们公司的机器节点超过了1024个,而你们的业务没有达到1毫秒需要2408个序列号的并发的情况下,可以将序列号的位数减少,给机器id增加位数。

4、序列号,取值在0~4096之间,最多支持一毫秒内生成4096个序列号。
假设你们公司的机器节点没有超过512个,而你们的业务1毫秒需要超过4096个序列号的并发的情况下,可以将机器id的位数减少,给序列号增加位数。

优点:
1、完全基于内存,ID生成效率高。
2、生成的ID基于时间戳和序列号,具有有序性的特点,方便排序、查询。
3、可以根据生产机器节点数和业务并发量的情况,调整ID的生成策略。

代码:

package com.jiepos.api.demo;

/**
 * 雪花算法
 * 64位ID = 1位符号位(固定为0,表示正数) + 41位时间戳 + 10位工作机器id + 12位序列号
 * @author shuyan.qi
 * @date 2020/5/3 9:42 下午
 */
public class SnowFlakeID {
    //12位序列号
    private long sequence;//序列号
    private long sequenceBits = 12L;//序列号位数
    private long maxSequence = -1 ^ (-1 << sequenceBits);//序列号最大值

    //10位工作机器id = 5位机房id + 5位机器id
    private long workerId;//机器id
    private long workerIdBits = 5L;//机器id位数
    private long maxWorkerId = -1 ^ (-1 << workerIdBits);//机器id最大值
    private long workerIdMoveBits = sequenceBits;//机器id左移位数 = 序列号位数
    private long workerIdAfterMove = 0L;//左移后的机器id

    private long workerCenterId;//机房id
    private long workerCenterIdBits = 5L;//机房id位数
    private long maxWorkerCenterId = -1 ^ (-1 << workerCenterIdBits);//机房id最大值
    private long workerCenterIdMoveBits = workerIdBits + workerIdMoveBits;//机房id左移位数 = 机器id位数 + 序列号位数
    private long workerCenterIdAfterMove = 0L;//左移后的机房id

    //41位时间戳
    private long lastTimestamp = -1L;//默认-1L
    private long initTimestamp = 1588518046057L;//初始时间戳
    private long timestampMoveBits = workerCenterIdBits + workerCenterIdMoveBits;//时间戳左移位数 = 机房id位数 + 机器id位数 + 序列号位数

    public SnowFlakeID(long workerCenterId , long workerId){
        if(workerCenterId < 0 || workerCenterId > maxWorkerCenterId){
            throw new IllegalArgumentException("workerCenterId is illegal");
        }
        if(workerId < 0 || workerId > maxWorkerId){
            throw new IllegalArgumentException("workerId is illegal");
        }
        this.workerCenterId = workerCenterId;
        this.workerId = workerId;
        this.workerCenterIdAfterMove = this.workerCenterId << this.workerCenterIdMoveBits;
        this.workerIdAfterMove = this.workerCenterId << this.workerCenterIdMoveBits;
    }

   /**
     * 生成ID的核心方法
     */
    public synchronized long nextId(){
        long currentTimestamp = timestamp();
        if(currentTimestamp < lastTimestamp){
            String s = String.format("currentTimestamp is earlier than lastTimestamp,lastTimestamp=%s,currentTimestamp=%s",lastTimestamp,currentTimestamp);
            System.out.println(s);
            //throw new RuntimeException(s);
            // 时钟回拨后手动拨正。
            // 因为依赖lastTimestamp,所以重启后第一次就发生时钟回拨的情况无法处理。
            // 可以将lastTimestamp存放到redis之类第三方缓存中,但这样生成id的效率会降低,请开发者根据实际情况去选择。
            currentTimestamp = lastTimestamp;
        }
        if(currentTimestamp == lastTimestamp){
            //同一时间戳,序列号加1
            sequence = (sequence + 1) & maxSequence;
            if(sequence == 0L){
                //如果序列号加1后的值为0,表示当前时间戳内的序列号已用完,需要获取下一个时间戳
                currentTimestamp = nextTimestamp(currentTimestamp);
            }
        }else{
            sequence = 0L;//不同时间戳,重置序列号
        }
        lastTimestamp = currentTimestamp;//更新成功生成id的最新时间戳
        return ((currentTimestamp - initTimestamp) << timestampMoveBits) | workerCenterIdAfterMove | workerIdAfterMove | sequence;
    }

    /**
     * 获取timestamp的下一毫秒数
     * @param timestamp 当前毫秒数
     * @return
     */
    public long nextTimestamp(long timestamp){
        long timestamp1 = 0L;
        do{
            timestamp1 = timestamp();
        }while (timestamp >= timestamp1);
        return timestamp1;
    }

    /**
     * 获取当前时间戳
     * @return
     */
    public long timestamp(){
        return System.currentTimeMillis();
    }

    public static void main(String[] args) throws InterruptedException {

     /*
        //测试并发
        Map<String,Object> map = new ConcurrentHashMap<>();
        SnowFlakeID snowFlakeID = new SnowFlakeID(1, 1);
        for(int i = 0;i < 100;i++){
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0 ;j < 100000;j++){
                    map.put(String.valueOf(snowFlakeID.nextId()),1);
                }
            }).start();;
        }

        for(;;){
            TimeUnit.SECONDS.sleep(1);
            System.out.println("size="+map.size());
        }

    */


     /*
        //测试速度
        SnowFlakeID snowFlakeID = new SnowFlakeID(1, 1);
        long startTime = System.currentTimeMillis();
        for(int i = 0;i < 3000000;i++){
            snowFlakeID.nextId();
        }
        System.out.println("耗时:"+(System.currentTimeMillis() - startTime)/1000.0d + "秒");
     */
    }


作者:钢铁加鲁鲁_d59c
链接:https://www.jianshu.com/p/d230443d0e60
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted on 2021-06-23 16:45  白露~  阅读(625)  评论(0编辑  收藏  举报