分布式ID之雪花算法

分布式ID常见生成策略

分布式ID生成策略常见的有如下几种:

  • 数据库自增ID。
  • UUID生成。
  • Redis的原子自增方式。
  • 数据库水平拆分,设置初始值和相同的自增步长。
  • 批量申请自增ID。
  • 雪花算法。
  • 百度UidGenerator算法(基于雪花算法实现自定义时间戳)。
  • 美团Leaf算法(依赖于数据库,ZK)。

本文主要介绍SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。

其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,保持自增性且不重复。

雪花算法的结构:

雪花算法的结构如下:
在这里插入图片描述

主要分为 5 个部分:

  1. 是 1 个 bit:0,这个是无意义的。
  2. 是 41 个 bit:表示的是时间戳。
  3. 是 10 个 bit:表示的是机房 id,0000000000,因为我传进去的就是0。
  4. 是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,0000 0000 0000。

接下去我们来解释一下四个部分:

  • 1 bit,是无意义的:

因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

  • 41 bit:表示的是时间戳,单位是毫秒。

41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。

  • 10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。

但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器),这里可以随意拆分,比如拿出4位标识业务号,其他6位作为机器号。可以随意组合。

  • 12 bit:这个是用来记录同一个毫秒内产生的不同 id。

12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。也就是同一毫秒内同一台机器所生成的最大ID数量为4096

  • 简单来说,你的某个服务假设要生成一个全局唯一 id,那么就可以发送一个请求给部署了 SnowFlake 算法的系统,由这个 SnowFlake 算法系统来生成唯一 id。这个 SnowFlake 算法系统首先肯定是知道自己所在的机器号,(这里姑且讲10bit全部作为工作机器ID)接着 SnowFlake 算法系统接收到这个请求之后,首先就会用二进制位运算的方式生成一个 64 bit 的 long 型 id,64 个 bit 中的第一个 bit 是无意义的。接着用当前时间戳(单位到毫秒)占用41 个 bit,然后接着 10 个 bit 设置机器 id。最后再判断一下,当前这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最后的 12 个 bit。

代码实现:

代码中将 10 bit 拆分成 5bit表示工作机器ID,5bit表示数据中心ID。

@Slf4j
public class SnowflakeIdWorker {

    // ==============================Fields===========================================
    /** 开始时间戳 (2015-01-01) */
    private final long twepoch = 1420041600000L;

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

    /** 数据标识id所占的位数 */
    private final long datacenterIdBits = 5L;

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

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

    /** 支持的最大数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

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

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

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

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

    /** 工作机器ID(0~31) */
    private long workerId;

    /** 数据中心ID(0~31) */
    private long datacenterId;

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

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

    private final long maxAreaId = -1L ^ (-1L << datacenterIdBits);//最大能够分配的Areaid =31

    /**机器名**/
    private String hostName;
    /**应用名**/
    private String appName;
    @Value("${deploy.region}")
    private String areaName;
    @Value("${server.port}")
    private String port;
    @Value("${zookeeper.address}")
    private String zk;

    public boolean init() {

        appName  = Utils.getAppName();
        if(null == port)
            port    = Utils.getPort();
        hostName = Utils.getHostName();
        Assert.notNull(appName,"appName is null");
        Assert.notNull(hostName,"hostName is null");
        Assert.notNull(areaName,"areaName is null");
        Assert.notNull(port,"port is null");

        SnowflakeZookeeperHolder snowflakeZookeeperHolder = new SnowflakeZookeeperHolder(hostName,port,zk,"global",(int)maxWorkerId,appName);
        boolean init = snowflakeZookeeperHolder.init();
        Assert.isTrue(init,"init workId error!!!");

        workerId = snowflakeZookeeperHolder.getWorkerID();

        if(Utils.areaMap.containsKey(areaName.toUpperCase())) {
            datacenterId = Utils.areaMap.get(areaName.toUpperCase()).shortValue();
        }
        Assert.isTrue(datacenterId>0,"get areaId error!!!");

        Assert.isTrue(workerId >= 0 && workerId <= maxWorkerId , "workerID must gte 0 and lte "+maxWorkerId);
        Assert.isTrue(datacenterId >= 0 && datacenterId <= maxAreaId , "areaId must gte 0 and lte "+maxAreaId);
        log.info("snowflake global info:workId={} areaId={}",workerId,datacenterId);
        return true;
    }

    //==============================Constructors=====================================
    /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    // ==============================Methods==========================================
    /**
     * 获得下一个ID (该方法是线程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间戳
        lastTimestamp = timestamp;

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

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间戳
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    //==============================Test=============================================
    /** 测试 */
    public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
        for (int i = 0; i < 1000; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
        }
    }
}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.RetryUntilElapsed;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SnowflakeZookeeperHolder {
    private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeZookeeperHolder.class);
    private String  zk_AddressNode  = null;
    private String  listenAddress   = null;
    private int     workerID;
    private static  final String PREFIX_ZK_PATH = "/snowflake";
    private static  String PATH_FOREVER;
    private String  connectionString;
    private long    lastUpdateTime;
    private String     port;
    private int     maxWordId;
    private String  lockPath;

    public SnowflakeZookeeperHolder(String hostName,String port, String connectionString,String snowTye,int maxWordId,String app) {
        this.listenAddress = hostName+":"+port;
        this.port = port;
        this.connectionString = connectionString;
        PATH_FOREVER = PREFIX_ZK_PATH + "/"+snowTye+"/"+app;
        this.maxWordId = maxWordId;
        this.lockPath = PREFIX_ZK_PATH + "/"+snowTye;
    }

    public boolean init() {
        try {
            CuratorFramework curator = createWithOptions(connectionString, new RetryUntilElapsed(1000, 4), 10000, 6000);
            curator.start();

            InterProcessMutex lock = new InterProcessMutex(curator,lockPath);

            try {
                if(lock.acquire(2,TimeUnit.SECONDS)) {
                    Stat stat = curator.checkExists().forPath(PATH_FOREVER);
                    if (stat == null) {
                        //不存在根节点,机器第一次启动,并上传数据
                        zk_AddressNode = createNode(curator);
                        //保存workID
                        updateNewData(curator, zk_AddressNode);
                    }else {
                        ArrayList<Integer> usedWorkIddSet= new ArrayList<>();
                        //存在根节点,先检查是否有属于自己的根节点
                        List<String> keys = curator.getChildren().forPath(PATH_FOREVER);
                        //临时节点未找到当前节点
                        if(!keys.contains(listenAddress)) {
                            for (String key : keys) {
                                Endpoint endpoint = getNodeData(curator, PATH_FOREVER + "/" + key);
                                if (null != endpoint) {
                                    usedWorkIddSet.add(endpoint.getWorkId());
                                }
                            }
                            if(keys.size() == 0){
                                zk_AddressNode = createNode(curator);
                                updateNewData(curator, zk_AddressNode);
                            }else {
                                ArrayList<Integer> allWordId = new ArrayList<>(maxWordId);
                                for (int i = 0; i <= maxWordId; i++) {
                                    allWordId.add(i);
                                }
                                allWordId.removeAll(usedWorkIddSet);
                                if(allWordId.size()<=0){
                                    LOGGER.error("workId全部被占用");
                                    return false;
                                }else{
                                    workerID = allWordId.get(0);
                                    zk_AddressNode = createNode(curator);
                                    //保存workID
                                    updateNewData(curator, zk_AddressNode);
                                }
                            }
                        }else{
                            zk_AddressNode = PATH_FOREVER + "/" + listenAddress;
                            Endpoint endpoint = getNodeData(curator, zk_AddressNode);
                            workerID = endpoint.getWorkId();
                            updateNewData(curator, zk_AddressNode);
                        }
                    }
                }else{
                    LOGGER.error("get zk lock fail!!!");
                    return false;
                }
            }catch (Exception e){
                LOGGER.error("get zk lock ERROR {}", e);
                return false;
            }finally {
                lock.release();
            }

        } catch (Exception e) {
            LOGGER.error("Start node ERROR {}", e);
            return false;
        }
        LOGGER.info(PATH_FOREVER+" workId="+workerID);
        return true;
    }


    private Endpoint getNodeData(CuratorFramework curator, String zk_AddressNode) throws Exception {
        byte[] bytes = curator.getData().forPath(zk_AddressNode);
        Endpoint endPoint = deBuildData(new String(bytes));
        return endPoint;
    }

    /**
     * 创建持久顺序节点 ,并把节点数据放入 value
     *
     * @param curator
     * @return
     * @throws Exception
     */
    private String createNode(CuratorFramework curator) throws Exception {
        try {
            return curator.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(PATH_FOREVER + "/" + listenAddress, buildData().getBytes());
        } catch (Exception e) {
            LOGGER.error("create node error msg {} ", e.getMessage());
            throw e;
        }
    }

    private void updateNewData(CuratorFramework curator, String path){
        try {
            if (System.currentTimeMillis() < lastUpdateTime) {
                return;
            }
            curator.setData().forPath(path, buildData().getBytes());
            lastUpdateTime = System.currentTimeMillis();
        } catch (Exception e) {
            try {
                init();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            LOGGER.info("update init data error path is {} error is {}", path, e);
        }
    }

    /**
     * 构建需要上传的数据
     *
     * @return
     */
    private String buildData() throws JsonProcessingException {
        Endpoint endpoint = new Endpoint(workerID, System.currentTimeMillis());
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(endpoint);
        return json;
    }

    private Endpoint deBuildData(String json) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        Endpoint endpoint = mapper.readValue(json, Endpoint.class);
        return endpoint;
    }


    private CuratorFramework createWithOptions(String connectionString, RetryPolicy retryPolicy, int connectionTimeoutMs, int sessionTimeoutMs) {
        return CuratorFrameworkFactory.builder().connectString(connectionString)
            .retryPolicy(retryPolicy)
            .connectionTimeoutMs(connectionTimeoutMs)
            .sessionTimeoutMs(sessionTimeoutMs)
            .build();
    }

    /**
     * 上报数据结构
     */
    static class Endpoint {
        private int workId;
        private long timestamp;

        public Endpoint() {
        }

        public Endpoint(int workId, long timestamp) {
            this.workId = workId;
            this.timestamp = timestamp;
        }

        public int getWorkId() {
            return workId;
        }

        public void setWorkId(int workId) {
            this.workId = workId;
        }

        public long getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }
    }

    public String getZk_AddressNode() {
        return zk_AddressNode;
    }

    public void setZk_AddressNode(String zk_AddressNode) {
        this.zk_AddressNode = zk_AddressNode;
    }

    public String getListenAddress() {
        return listenAddress;
    }

    public void setListenAddress(String listenAddress) {
        this.listenAddress = listenAddress;
    }

    public int getWorkerID() {
        return workerID;
    }

    public void setWorkerID(int workerID) {
        this.workerID = workerID;
    }
}

总结

SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。但是依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化,成业务表或者和我们系统相关的业务。

其实对于分布式ID的生成策略。无论是我们上述提到的哪一种。无非需要具有以下两种特点。 自增的、不重复的。而对于不重复且是自增的,那么我们是很容易想到的是时间,而雪花算法就是基于时间戳。但是毫秒级的并发下如果直接拿来用,显然是不合理的。那么我们就要在这个时间戳上面做一些文章。至于怎么能让这个东西保持唯一且自增。就要打开自己的脑洞了。可以看到雪花算法中是基于 synchronized 锁进行实现的。

鸣谢

雪花算法原理解析

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱coding的同学

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

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

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

打赏作者

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

抵扣说明:

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

余额充值