雪花算法是一个分布式ID的生成算法,能够生成全局唯一和有序递增的id,生成的时候不依赖于其他中间件,仅仅一个工具类即可生成,这在单体应用中十分好用,但是在微服务上就有些局限了,因为雪花算法依赖于机器id的不同,才能保证多台机器上生成的id不重复。
为了解决多台机器上生成id重复的问题,我们可以在每台机器部署服务的时候,手动指定不同的机器id到环境变量,并赋值到雪花id生成器中,虽然这是一个解决办法,但并不高明,因为我们不仅要拿个本子手动记录每台服务器的机器id,还要逐台服务器的去处理。
为此社区内出现了许多解决办法,其中百度、滴滴、美团都推出了自己的id生成器方案,有雪花算法的变种,有基于数据库号段模式的改造,都是不错的处理方式。
本文所分享的是定时任务结合Redis分布式锁实现的机器id的获取和回收。
实现步骤
-
提前在Redis中设置好可用的机器id范围,如有1024个机器id可用:
shellEVAL "for i=1,1023 do redis.call('SADD', 'worker_id_pool', i) end" 0
这会在Redis中保存一个集合,里面是可用的机器码。
-
Spring Boot程序启动,定时任务执行,去向Redis获取一个未被使用的机器id:
javafinal Boolean flag = redisTemplate.opsForValue().setIfAbsent(useKey, workerId); if (flag != null && flag) { log.info("保存机器id成功:{}", workerId); redisTemplate.expire(useKey, 1, TimeUnit.HOURS); IdGenerator.setWorkerId(workerId); } else { log.info("保存机器id失败,该机器id已被使用:{}", workerId); }
关于机器id回收
申请得到的机器id要设置一个过期时间,然后定时任务每次都向Redis发起一个更新操作,保持这个机器id是一直被使用的状态。
代码
java
import com.cc.utils.IdGenerator;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 雪花id获取机器id后定时的心跳机制,保证机器id唯一,如果停止心跳,Redis可以认为该机器id可以回收
* 需要提前在Redis上创建好机器id池,操作命令为:EVAL "for i=1,1023 do redis.call('SADD', 'worker_id_pool', i) end" 0
*
* @author cc
* @date 2023-09-19 15:32
*/
public class IdHeartbeatJob extends QuartzJobBean {
/**
* 机器id池
*/
private static final String WORKER_ID_POOL = "worker_id_pool";
/**
* 被申请使用着的机器id池
*/
private static final String WORKER_ID_POOL_USED = "worker_id_pool_used";
private static final Logger log = LoggerFactory.getLogger(IdHeartbeatJob.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
/**
* 判断是否有workerId
* 没有:
* 申请
* 有:
* 发送心跳,重置过期时间
*/
log.info("定时向Redis发送雪花id申请心跳");
long workerId = IdGenerator.INSTANCE.getWorkerId();
if (workerId == 0) {
log.info("本机器id为0,开始申请机器id");
final Set<Object> ids = redisTemplate.opsForSet().members(WORKER_ID_POOL);
if (ids == null || ids.isEmpty()) {
log.error("申请机器id失败,请检查Redis中是否有机器id池");
return;
}
Set<String> keys = redisTemplate.keys(WORKER_ID_POOL_USED + "*");
if (keys == null) {
keys = new HashSet<>();
}
String useKey = null;
for (Object item : ids) {
useKey = WORKER_ID_POOL_USED + "_" + item;
boolean useFlag = false;
for (String key : keys) {
if (useKey.equals(key)) {
useFlag = true;
break;
}
}
if (useFlag) {
continue;
}
workerId = ((Number) item).longValue();
break;
}
if (workerId == 0) {
log.error("无可用的机器id");
return;
}
log.info("有id可用:{}", workerId);
/**
* 用分布式锁避免并发问题
*/
final Boolean flag = redisTemplate.opsForValue().setIfAbsent(useKey, workerId);
if (flag != null && flag) {
log.info("保存机器id成功:{}", workerId);
redisTemplate.expire(useKey, 1, TimeUnit.HOURS);
IdGenerator.setWorkerId(workerId);
} else {
log.info("保存机器id失败,该机器id已被使用:{}", workerId);
}
} else {
log.info("有机器id:{},发送心跳保存", workerId);
String useKey = WORKER_ID_POOL_USED + "_" + workerId;
redisTemplate.opsForValue().set(useKey, workerId);
redisTemplate.expire(useKey, 1, TimeUnit.HOURS);
}
}
}
总结
这种方式的好处是逻辑简单,目的是为了解决手动操作每台服务器的工作。
雪花id虽好,但也有他的毛病,比如时钟回拨、id太长等,所以根据项目情况,也需要考虑使用其他的id生成器。