中小型分布式系统分布式ID生产实战
在分布式id生产实战之前,先来给广大后端程序员童鞋们推荐一个小而美的工具包-Hutool,因为我们目前生产用到的分布式id生成就是基于该包工具类Snowflake(雪花id)而生成.
Hutool 是一个小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语 言也可以“甜甜的”.Hutool 对文件、流、加密解密、转码、正则、线程、XML、日期、Http 客户端 等 JDK 方法进行封装,组成各种 Util 工具类.它涵盖了 Java 开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当;它是项目中 “util” 包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的 bug.
雪花算法(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;
@Getter
public 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
@Slf4j
public 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()
该模式经过生产多实例高并发验证.算是个可靠的解决方案.
喜欢就关注下吧
更多实战,等你来看
外卖领神卷,更省更划算