1. 什么是分布式ID
在分布式系统中,通常需要针对一些数据做全局唯一标识,这个全局唯一标识就是分布式ID。
2. 分布式ID应该满足哪些基本条件
2.1 全局唯一:纵观全局,必须是唯一,不可重复的。
2.2 高性能:ID生成一定要快,高吞吐,低延时。
2.3 趋势递增:递增有利于数据库中索引的维护。
2.4 易读性:如果ID没有生成规则,很容易造成错误率,具体情况需要根据业务场景来定。
3. 分布式ID生成方式
网络上生成分布式ID的方案真的是太多了,像UUID、数据库自增ID、Redis、雪花算法(SnowFlake)、美团(Leaf)、滴滴(Tinyid)等等。
自己项目最终选择的是雪花算法,主要考虑到该算法只需要依赖内存就可以实现,而且是递增的数字,最多能表示的机器数量是1024个足够满足项目所需,故这里简单说下雪花。
4. 雪花算法
这里直接使用hutool工具包下提供的,依赖如下
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.5</version>
</dependency>
5. 落地
分布式系统考虑到有多个服务,每个服务又有多个实例(自己项目当中每个服务只有三个实例),这里利用 Snowflake(long workerId, long dataCenterId)构造方法。
/**
* 构造
*
* @param workerId 终端ID
* @param dataCenterId 数据中心ID
*/
public Snowflake(long workerId, long dataCenterId) {
this(workerId, dataCenterId, false);
}
5.1
这里假设后续业务激增,每个服务最多也不会超过10个实例,自己项目当中是一共有15个服务,将每个服务划分出一段区间,比如用户服务 是1-10、卡户服务是2-20、消息推送服务是3-30等依次排下去作为终端ID,同时在每个服务的配置文件里配置不同服务的标识作为构造方法的数据中心ID。
5.2
每个服务启动的时候,在Bean初始化的过程中利用 spring提供的扩展点 @PostConstruct ,读取自己服务配置文件里设置的终端ID,假设用户服务1-10,那么用户服务的第一个实例启动的时候就是1,紧接着第二个实例启动的时候就是2,第三个实例启动的时候就是3,当频繁启动用户服务的时候,达到最大值10,我们让其循环,再次从1-10开始。
5.3
初始化服务实例数
/**
* Created with IntelliJ IDEA.
*
* @Author: zhaoxn
* @Date: 2022/11/15/20:02
* @Description:初始化服务实例数
*/
public class IninServiceInstanceCount {
private int currCount;//当前实例数值
private int START_POSITION = 1; //实际从配置文件中获取,可以将该类放到一个公用的模块
private int END_POSITION = 10; //实际从配置文件中获取,可以将该类放到一个公用的模块
@Autowired
private RedisTemplate redisTemplate;
private String CURRINST_ANCE_KEY = ":sequence:globalSerialNumber";
@Value("${spring.application.name}")
private String appName;
@PostConstruct
public void initParam(){
Object object = redisTemplate.opsForValue().get(appName + CURRINST_ANCE_KEY);
Integer currCacheKey = (Integer) object;
if (currCacheKey == null){
//实例起始位置
currCount = START_POSITION;
redisTemplate.opsForValue().set(appName + CURRINST_ANCE_KEY,START_POSITION);
}else{
if (currCacheKey >= END_POSITION){
currCount = START_POSITION;
redisTemplate.opsForValue().set(appName + CURRINST_ANCE_KEY,START_POSITION);
}else{
++currCacheKey;
currCount = currCacheKey;
redisTemplate.opsForValue().increment(appName + CURRINST_ANCE_KEY);
}
}
}
public int getCurrCount() {
return currCount;
}
public void setCurrCount(int currCount) {
this.currCount = currCount;
}
}
5.4
雪花算法工具类
/**
* Created with IntelliJ IDEA.
*
* @Author: zhaoxn
* @Date: 2022/11/15/20:04
* @Description:雪花算法工具类
*/
@Slf4j
public class SonwFlakeUtils {
private Snowflake snowflake;
public String getGlobalSerialNumber(String serviceNo,long machineId,long dataCenterId){
log.info("当前服务编号:{},机器编号:{},机房id:{}",serviceNo,machineId,dataCenterId);
String res = "";
try{
if(null == snowflake){
synchronized (SonwFlakeUtils.class){
if (null == snowflake){
snowflake = new Snowflake(machineId,dataCenterId);
}
}
}
res = serviceNo + "yyMMdd" +(snowflake.nextId());
return res;
}catch (Exception e){
log.error("生成分布式id出现异常:",e);
}
return res;
}
}
5.5
使用的时候直接注入SonwFlakeUtils 类,通过调用IninServiceInstanceCount中的getCurrCount方法,获取machineId,同时获取每个服务配置文件中的dataCenterId,最后调用getGlobalSerialNumber(),传入参数,这里多了一个serviceNo参数,是自己项目区分不同服务的,最终生成分布式Id。
5.6
自己项目最后生成的规则是
共34位,服务编号(区分不同服务)3位 + yyMMdd + 6位 + 25 位雪花算法。