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

相關文章
相關標籤/搜索