查看原文
其他

中小型分布式系统分布式ID生产实战

老猿人 码农闲谈AI 2024-01-22

Hutool

     在分布式id生产实战之前,先来给广大后端程序员童鞋们推荐一个小而美的工具包-Hutool,因为我们目前生产用到的分布式id生成就是基于该包工具类Snowflake(雪花id)而生成.

     Hutool 是一个小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语 言也可以“甜甜的”.Hutool 对文件、流、加密解密、转码、正则、线程、XML、日期、Http 客户端 等 JDK 方法进行封装,组成各种 Util 工具类.它涵盖了 Java 开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当;它是项目中 “util” 包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的 bug.

雪花id

    ‍‍‍雪花算法(Snowflake)是一种分布式唯一ID生成算法,它可以在分布式系统中生成全局唯一的ID.该算法由Twitter开发,用于解决分布式系统中生成唯一ID的需求.雪花算法的核心思想是将一个64位的ID分成多个部分,每个部分表示不同的信息.具体来说,一个雪花ID由以下几个部分组成:

    0初始位:占一位,默认为0.二进制里第一个 bit 为如果是1,那,都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0

    时间戳:占用41位,表示生成ID的时间戳,精确到毫秒级别.这样可以保证在同一毫秒内生成的ID是唯一的.

    机器ID:占用10位,表示生成ID的机器的唯一标识.在分布式系统中,每台机器都需要有一个唯一的标识,以防止生成重复的ID.‍‍‍

    序列号:占用12位,表示同一毫秒内生成的多个ID的序列号.当同一毫秒内生成的ID超过4096个时,序列号会从0开始重新计数

    优点:生成的ID有序、趋势递增,并且在分布式系统中具有较高的性能和可靠性

    缺点:雪花算法并不是绝对的全局唯一,因为机器ID需要保证唯一性,而且系统时钟需要保证准确性.如果机器ID重复或者系统时钟回拨,都有可能导致生成重复的ID.因此,在使用雪花算法时,需要确保机器ID的唯一性,并且对系统时钟进行合理的同步和校准.

生产实战

maven引入相关包

<dependency>  <groupId>cn.hutool</groupId>  <artifactId>hutool-all</artifactId>  <version>4.5.11</version></dependency><dependency>  <groupId>org.apache.zookeeper</groupId>  <artifactId>zookeeper</artifactId>  <version>3.5.6</version></dependency>


在使用Hutool包中的Snowflake时,我们可以看到构造函数

public Snowflake(long workerId, long datacenterId){   this(workerId, datacenterId, false);}

这其中的两个参数workerId及datacenterId分别代表的就是节点id及数据中心id.那么这两个参数我们要怎么定义及使用它们呢?

假如我们现在分布式系统中有test-user、test-order、test-tenant三个模块.且这三个模块都需要生成id.那么可以在common包中定义一个枚举,用枚举来定义每个模块的datacenterId.

package com.common;import cn.hutool.core.util.StrUtil;import lombok.Getter;@Getterpublic enum DataCenterIdEnums {    TEST-USER("test-user", 0),    TEST-ORDER("test-order", 1),    TEST-TENANT("test-tenant", 2),    ;    private String applicationName;    private long dataCenterId;    DataCenterIdEnums(String appName, long dataCenterId) {        this.appName = appName;        this.dataCenterId = dataCenterId;    }    public static long getCenterId(String applicationName) throws Exception{        DataCenterIdEnums[] dataCenterIdEnums = DataCenterIdEnums.values();        for (DataCenterIdEnums dci : dataCenterIdEnums) {            if (StrUtil.equals(dci.getApplicationName(), applicationName)) {                return dci.getDataCenterId();            }        }        throw new Exception("找不到该应用centerId");    }}

这样模块的datacenterId就定义好了,那么workerId又如何定义呢?在分布式系统中,几乎所有模块都是可以根据业务请求量进行动态的缩容扩容,所以每个模块的实例数量都是不固定的.那么这里我们就可以用到zookeeper的临时有序节点,每次在实例启动时都去zk创建一个临时有序节点,临时有序节点的好处就是当客户端失去连接(也就是当实例销毁时)会释放该临时节点.代码如下:

首先我们要创建个单例类,用来保存节点及数据中心对应的Snowflake对象

package com.common.utils;import cn.hutool.core.lang.Snowflake;public class SnowFlakeIdUtils {    private long workId;    private Snowflake snowflake;    private static SnowFlakeIdUtils snowFlakeIdUtils = new SnowFlakeIdUtils ();    public static SnowFlakeIdUtils getInstance() {        return snowFlakeIdUtils;    }    public long getWorkId() {        return workId;    }    public Snowflake getSnowflake() {        return snowflake;    }    public void setWorkId(long workId,long dataCenterId) {        this.workId = workId;        snowflake = new Snowflake(workId, dataCenterId);    }}

然后创建配置类,当应用启动时去zookeeper生成该应用实例的临时有序节点

package com.common.config;import com.alibaba.fastjson.JSON;import com.common.enums.DataCenterIdEnums;import com.common.utils.WorkIdUtils;import lombok.extern.slf4j.Slf4j;import org.apache.zookeeper.*;import org.apache.zookeeper.data.Stat;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.text.DecimalFormat;import java.util.Comparator;import java.util.List;import java.util.concurrent.CountDownLatch;@Configuration@Slf4jpublic class SnowFlakeIdConfig {    private static CountDownLatch connectedSemaphore = new CountDownLatch(1);    @Value("${spring.application.name}")    private String applicationName;    private final static String TEMP = "0000000000";    @Value("${zookeeper.host}")    private String host;    private final static int ZOOKEEPER_TIMEOUT= 60000;    public void generateId() throws Exception{        try {            ZooKeeper zookeeper = new ZooKeeper(host, ZOOKEEPER_TIMEOUT, new Watcher() {                @Override                public void process(WatchedEvent watchedEvent) {                    //链接成功                    connectedSemaphore.countDown();                }            });            //该应用节点路径            String nodePath = "/" + applicationName;            //zookeeper是异步链接,等待zookeeper连接成功才能做以下处理            connectedSemaphore.await();            Stat stat = zookeeper.exists(nodePath, false);            if (stat == null) {                //如果没有应用目录,则要创建应用的的持久目录                zookeeper.create(nodePath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);            }            //获取应用路径下的子路径            List<String> pathList = zookeeper.getChildren(nodePath, false);            //获取到后升序排序            pathList.sort(Comparator.naturalOrder());            String workerId = null;            //此时应为不知道哪些临时有序节点时被占用的,所以循环判断子路径是否已经存在            for (int i = 0; i < Integer.MAX_VALUE; i++) {                if(i == pathList.size() ||  i != Integer.valueOf(pathList.get(i))) {                    workerId = transStr(i);                    String genPath = nodePath + "/" + workerId;                    //创建临时序号节点                    zookeeper.create(genPath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);                    break;                }            }            SnowFlakeIdUtils.getInstance().setWorkId(Long.parseLong(workerId), DataCenterIdEnums.getCenterId(applicationName));        } catch (Exception e) {            throw new Exception("zookeeper连接失败了");        }    }    private static String transStr(long value) {        return new DecimalFormat(TEMP_START).format(value);    }}

这样我们每个应用动态扩容的时候,就会在zookeeper下面创建一个对应的临时有序节点,缩容时会自动释放掉而不占用节点,可以供下次扩容使用.定义好以后接下来我们就可以分别在test-user,test-order,test-tenant中去使用SnowFlakeIdUtils生成我们的分布式id了

Long id = SnowFlakeIdUtils.getInstance().getSnowflake().nextId()

该模式经过生产多实例高并发验证.算是个可靠的解决方案.




喜欢就关注下吧

更多实战,等你来看

外卖领神卷,更省更划算

继续滑动看下一个

中小型分布式系统分布式ID生产实战

老猿人 码农闲谈AI
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存