雪花算法生成id重复问题
我们之前提到,同一机器同一毫秒级,我们能生成4096个不同序列,即不同Id,但是如果我们使用的是微服务架构,那不同机器人是否会可能生成相同Id呢?其实我们之前有提到工作机器Id的作用,就是用于解决分布式Id重复的问题,这个workerId是通过构造方法传入的,如果我们用10位来存储这个值,那就是最多支持1024个节点那么关键问题就回归到如何去把我们的服务器和workerId对应起来?
为何重复
我们之前精通分布式,没听过SnowFlake?中提到,雪花算法在同一机器同一毫秒级,我们能生成4096个不同序列(12bit序列号情况下),即不同Id,但是如果我们使用的是微服务架构,那不同机器人是否会可能生成相同Id呢?
其实我们之前有提到工作机器Id的作用,就是用于解决分布式Id重复的问题,这个workerId是通过构造方法传入的,如果我们用10位来存储这个值,那就是最多支持1024个节点
/**
* 构造函数
* @param workerId 工作ID (0~1023)
*/
public SnowflakeIdWorker(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
}
this.workerId = workerId;
}
那么关键问题就回归到如何去把我们的服务器和workerId对应起来?如果不是容器化部署,部署是固定的机器,我们用机器的唯一名来做key,那我们可以对这些机器名和workerId建立一个对应关系,如果存在就用之前的workerId,不存在就往上累加比如我们用计算机名做key:
这样机器如果不断累加,最多支持1024台服务器,但是如果是容器化部署,需要支持动态增加节点,并且每次部署的机器不一定一样时,就会有问题,如果发现不同,就往上累加,经过多次发版,就可能会超过1023,这个时候生成雪花Id时,工作机器id左移12位后,当进行或运算时,时间戳的位置就会被影响,比如workerId=1024,我们拿之前的举例第1000ms,那它和第1001ms、workerId=0配置,可能生成重复的Id。来看看下面生成workerId的Demo:
private static void Init()
{
if (worker == null)
{
//初始化为1
long workerId = 1;
//得到服务器机器名称
string hostName = System.Net.Dns.GetHostName();
if (RedisHelper.Exists(hostName))
{
// 如果redis中存在改服务器名称,则直接取得workerId
workerId =long.Parse(RedisHelper.Get(hostName));
}
else
{
//如果redis不存在,则用hashcode对32取模
var code = hostName.GetHashCode();
var Id = code % 32;
//如果取模以后的Id,大于15,则从0~15中随机一个数字,也就是把16~31中转换到0~15,并存入redis
//这里只给了4个bit存储workerId,所以只能支持0~15
if (Id>15||Id<0)
{
Id = new Random().Next(0, 15);
}
workerId = (long)Id;
RedisHelper.Set(hostName, workerId);
}
//把workerId传入构造方法
worker = new IdWorker(workerId);
}
}
上述代码有2个问题:
- hashcode对32取模,本身就可能会重复,比如460141958和3164804对32取模都是4,那生成的workerId就重复了
- 如果hashcode>15,随机取一个,那每次都有1/16的概率重复
如何解决重复
工作机器id:10bit,表示工作机器id,用于处理分布式部署id不重复问题,可支持2^10 = 1024个节点,我们只需要给同一个微服务分配不同的工作机器ID即可,在redis中存储一个当前workerId的最大值。每次生成workerId时,从redis中获取到当前workerId最大值,并+1作为当前workerId,并存入redis。如果workerId为1023,自增为1024,则重置0,作为当前workerId,并存入redis。
@Component
public class IdWorkConfig {
private final RedisTemplate<String, Object> redisTemplate;
private final String applicationName;
public IdWorkConfig(RedisTemplate<String, Object> redisTemplate,
@Value("${spring.application.name}") String applicationName) {
this.redisTemplate = redisTemplate;
this.applicationName = applicationName;
}
/**
* 自定义workerId,保证该应用的ID不会重复
*
* @return 新的id生成器
*/
@Bean
public DefaultIdentifierGenerator defaultIdentifierGenerator() {
String MAX_ID = applicationName + "-worker-id";
Long maxId = this.getWorkerId(MAX_ID);
String maxIdStr = Long.toBinaryString(maxId);
// 将数据补全为10位
maxIdStr = StringUtils.leftPad(maxIdStr, 10, "0");
// 从中间进行拆分
String datacenterStr = maxIdStr.substring(0, 5);
String workerStr = maxIdStr.substring(5, 10);
// 将拆分后的数据转换成dataCenterId和workerId
long dataCenterId = Integer.parseInt(datacenterStr, 2);
long workerId = Integer.parseInt(workerStr, 2);
return new DefaultIdentifierGenerator(workerId, dataCenterId);
}
/**
* LUA脚本获取workerId,保证每个节点获取的workerId都不相同
*
* @param key 当前微服务的名称
* @return workerId
*/
private Long getWorkerId(String key) {
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redis_worker_id.lua")));
redisScript.setResultType(Long.class);
return redisTemplate.execute(redisScript, Collections.singletonList(key));
}
}
lua脚本如下
local isExist = redis.call('exists', KEYS[1])
if isExist == 1 then
local workerId = redis.call('get', KEYS[1])
workerId = (workerId + 1) % 1024
redis.call('set', KEYS[1], workerId)
return workerId
else
redis.call('set', KEYS[1], 0)
return 0
end
更多推荐
所有评论(0)