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