Redis解决雪花算法(snowflake)dataId和workId的自动选择分发问题

Redis解决雪花算法(snowflake)dataId和workId的自动选择分发问题

1.简介

在分布式系统中,使用snowflake算法生成全局惟一标识符是很是方便的,可是snowflake算法有一个须要注意的地方,就是它拥有两个变量datacenterId和workerId,snowflake经过这两个变量来实现分布式系统下每一个服务生成不重复的ID, 可是这两个变量取值必须在 1 - 31之间(包括),也就是snowflake最多只能支持31*31=961个节点同时使用,且每一个节点的datacenterId和workerId不能重复,接下来我分享一个经过redis实现的datacenterId和workerId分发的算法,请指正:java

2.定义SnowflakeInitiator类,实现snowflake的机器码的分发:

/**
 * 雪花算法初始化器
 * 初始化snowflake的dataCenterId和workerId
 * <p>
 * 1.系统启动时生成默认dataCenterId和workerId,并尝试做为key存储到redis
 * 2.若是存储成功,设置redis过时时间为24h,把当前dataCenterId和workerId传入snowflake
 * 3.若是存储失败workerId自加1,并判断workerId不大于31,重复1步骤
 * 4.定义一个定时器,每隔24h刷新redis的过时时间为24h
 *
 * @CreatedBy: yangcan 2020/5/13 14:05
 * @Description:
 */
@Component
//@Configuration
public class SnowflakeInitiator {

    /**
     * snowflake的dataCenterId和workerId
     */
    public static SnowflakeVo snowflakeVo;
    private static String prefixRedisKey = "YC_SnowflakeRedisKey";
    private static String snowflakeRedisKey;
    private static long LockExpire = 60 * 60 * 24;
    private static boolean stopTrying = false;
    @Autowired
    private RedisTemplate redisTemplate;

    public void init() throws InterruptedException {
        if (stopTrying) {
            System.out.println("snowflake强制结束生成key,key = " + JSON.toJSONString(snowflakeVo));
            return;
        }
        if (tryInit()) {
            System.out.println("snowflake结束生成key,key = " + JSON.toJSONString(snowflakeVo));
            return;
        }
        Thread.sleep(10);
        init();
    }

    public boolean tryInit() {
        snowflakeVo = nextKey(snowflakeVo);
        snowflakeRedisKey = prefixRedisKey + "_" + snowflakeVo.getDataCenterId() + "_" + snowflakeVo.getWorkerId();
        if (redisTemplate.hasKey(snowflakeRedisKey) == false) {
            if (redisTemplate.opsForValue().setIfAbsent(snowflakeRedisKey, 1, LockExpire, TimeUnit.SECONDS)) {
                System.out.println("成功抢占锁,Constants.snowflakeVo = " + JSON.toJSONString(snowflakeVo));
                return true;
            }
        }

        return false;
    }

    /**
     * 生成下一组不重复的dataCenterId和workerId
     *
     * @return
     */
    private SnowflakeVo nextKey(SnowflakeVo snowflakeVo) {
        if (snowflakeVo == null) {
            return new SnowflakeVo(1L, 1L);
        }

        if (snowflakeVo.getWorkerId() < 31) {
            // 若是workerId < 31
            snowflakeVo.setWorkerId(snowflakeVo.getWorkerId() + 1);
        } else {
            // 若是workerId >= 31
            if (snowflakeVo.getDataCenterId() < 31) {
                // 若是workerId >= 31 && dataCenterId < 31
                snowflakeVo.setDataCenterId(snowflakeVo.getDataCenterId() + 1);
                snowflakeVo.setWorkerId(1L);
            } else {
                // 若是workerId >= 31 && dataCenterId >= 31
                snowflakeVo.setDataCenterId(1L);
                snowflakeVo.setWorkerId(1L);
                stopTrying = true;
            }
        }
        return snowflakeVo;
    }

    /**
     * 从新设置过时时间,由定时任务调用
     */
    public void resetExpire() {
        redisTemplate.expire(snowflakeRedisKey, (LockExpire - 600), TimeUnit.SECONDS);
        System.out.println("YC 执行定时任务重置snowflakeRedisKey过时时间 resetExpire() redisKey = " + snowflakeRedisKey);
    }

    /**
     * 容器销毁时主动删除redis注册记录,此方法不适用于强制终止Spring容器的场景,只做为补充优化
     */
    public void destroy() {
        redisTemplate.delete(snowflakeRedisKey);
        System.out.println("YC destroy snowflakeRedisKey = " + redisKey);
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class SnowflakeVo {
        private Long dataCenterId;
        private Long workerId;
    }
}

3.定义定时任务,定时刷新redis的过时时间

@Component
public class MySchedule {

    @Autowired
    private SnowflakeInitiator snowflakeInitiator;

    @Scheduled(fixedDelay = 1000 * 60 * 60 * 23)
    private void snowflakeInitiator_ResetExpire() {
        snowflakeInitiator.resetExpire();
    }

}

4.用@PreDestroy注解实现,当服务中止后自动删除redis记录

/**
     *  在服务强制kill的状况下不触发@PreDestroy,此方法只做为补充方法使用
     */
    @PreDestroy
    public void destroy() {
        System.out.println("YC destroy something start");
        snowflakeInitiator.destroy();
        System.out.println("YC destroy something end");
    }

4.大体流程

  • 1.系统启动时生成默认dataCenterId和workerId,并尝试做为key存储到redis
  • 2.若是存储成功,设置redis过时时间为24h,把当前dataCenterId和workerId传入snowflake
  • 3.若是存储失败workerId自加1,并判断workerId不大于31,重复1步骤
  • 4.定义一个定时器,每隔24h刷新redis的过时时间为24h

5.缺陷

  • 1.服务启动的时候就尝试链接redis获取机器码,会形成服务启动比平时慢5s左右(具体看电脑配置)
  • 2.在tryInit()的过程当中,最多会重试961次(经过测试,重试1000次会延迟6s左右),也会形成服务启动慢
  • 3.当重试961次(即全部机器码都被占用了),系统会默认返回机器码1-1(这是snowflake硬伤,没办法,只能从自己系统上优化)
  • 4.当服务被强制kill掉时,@PreDestroy注解不会被触发,只能经过自己设置的过时日期(24h)等待过时(这个缺陷目前只想到了经过缩短过时日期优化)
  • 5.此方法依赖于spring的Bean注入方式保证单例,若是经过new SnowflakeInitiator()的方式实例化就会失效(能够自行优化或写成单例默认)
  • 6.不支持多线程调用,想多线程调用的本身优化

6.小结

每次用snowflake的时候都会有这方面的苦恼,此次提供的方式只是暂时解决了snowflake的问题,不过可能还有其余更优方式,作个调查:你们有没有尝试把ID生成模块独立出来作一个单独的服务,以供其余业务服务使用的?redis

相关文章
相关标签/搜索