为何重复

我们之前精通分布式,没听过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个问题:

  1. hashcode对32取模,本身就可能会重复,比如460141958和3164804对32取模都是4,那生成的workerId就重复了
  2. 如果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


 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐