基于Redis生成分布式全局唯一ID的3種策略
在分布式系統(tǒng)設(shè)計(jì)中,全局唯一ID是一個(gè)基礎(chǔ)而關(guān)鍵的組件。隨著業(yè)務(wù)規(guī)模擴(kuò)大和系統(tǒng)架構(gòu)向微服務(wù)演進(jìn),傳統(tǒng)的單機(jī)自增ID已無(wú)法滿足需求。高并發(fā)、高可用的分布式ID生成方案成為構(gòu)建可靠分布式系統(tǒng)的必要條件。
Redis具備高性能、原子操作及簡(jiǎn)單易用的特性,因此我們可以基于Redis實(shí)現(xiàn)全局唯一ID的生成。
分布式ID的核心需求
一個(gè)優(yōu)秀的分布式ID生成方案應(yīng)滿足以下要求
- 全局唯一性:在整個(gè)分布式系統(tǒng)中保證ID不重復(fù)
- 高性能:能夠快速生成ID,支持高并發(fā)場(chǎng)景
- 高可用:避免單點(diǎn)故障,確保服務(wù)持續(xù)可用
- 趨勢(shì)遞增:生成的ID大致呈遞增趨勢(shì),便于數(shù)據(jù)庫(kù)索引和分片
- 安全性(可選) :不包含敏感信息,不易被推測(cè)和偽造
1. 基于INCR命令的簡(jiǎn)單自增ID
原理
這是最直接的Redis分布式ID實(shí)現(xiàn)方式,利用Redis的INCR
命令原子性遞增一個(gè)計(jì)數(shù)器,確保在分布式環(huán)境下ID的唯一性。
代碼實(shí)現(xiàn)
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisSimpleIdGenerator { private final RedisTemplate<String, String> redisTemplate; private final String ID_KEY; public RedisSimpleIdGenerator(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; this.ID_KEY = "distributed:id:generator"; } /** * 生成下一個(gè)ID * @return 唯一ID */ public long nextId() { Long id = redisTemplate.opsForValue().increment(ID_KEY); if (id == null) { throw new RuntimeException("Failed to generate id"); } return id; } /** * 為指定業(yè)務(wù)生成ID * @param bizTag 業(yè)務(wù)標(biāo)簽 * @return 唯一ID */ public long nextId(String bizTag) { String key = ID_KEY + ":" + bizTag; Long id = redisTemplate.opsForValue().increment(key); if (id == null) { throw new RuntimeException("Failed to generate id for " + bizTag); } return id; } /** * 獲取當(dāng)前ID值但不遞增 * @param bizTag 業(yè)務(wù)標(biāo)簽 * @return 當(dāng)前ID值 */ public long currentId(String bizTag) { String key = ID_KEY + ":" + bizTag; String value = redisTemplate.opsForValue().get(key); return value != null ? Long.parseLong(value) : 0; } }
優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 實(shí)現(xiàn)極其簡(jiǎn)單,僅需一次Redis操作
- ID嚴(yán)格遞增,適合作為數(shù)據(jù)庫(kù)主鍵
- 支持多業(yè)務(wù)ID隔離
缺點(diǎn)
- Redis單點(diǎn)故障會(huì)導(dǎo)致ID生成服務(wù)不可用
- 主從切換可能導(dǎo)致ID重復(fù)
- 無(wú)法包含業(yè)務(wù)含義
適用場(chǎng)景
- 中小規(guī)模系統(tǒng)的自增主鍵生成
- 對(duì)ID連續(xù)性有要求的業(yè)務(wù)場(chǎng)景
- 單數(shù)據(jù)中心部署的應(yīng)用
2. 基于Lua腳本的批量ID生成
原理
通過(guò)Lua腳本一次性獲取一批ID,減少網(wǎng)絡(luò)往返次數(shù),客戶端可在內(nèi)存中順序分配ID,顯著提高性能。
代碼實(shí)現(xiàn)
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Component public class RedisBatchIdGenerator { private final RedisTemplate<String, String> redisTemplate; private final String ID_KEY = "distributed:batch:id"; private final DefaultRedisScript<Long> batchIncrScript; // 批量獲取的大小 private final int BATCH_SIZE = 1000; // 本地計(jì)數(shù)器和鎖 private AtomicLong currentId = new AtomicLong(0); private AtomicLong endId = new AtomicLong(0); private final Lock lock = new ReentrantLock(); public RedisBatchIdGenerator(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; // 創(chuàng)建Lua腳本 String scriptText = "local key = KEYS[1] " + "local step = tonumber(ARGV[1]) " + "local currentValue = redis.call('incrby', key, step) " + "return currentValue"; this.batchIncrScript = new DefaultRedisScript<>(); this.batchIncrScript.setScriptText(scriptText); this.batchIncrScript.setResultType(Long.class); } /** * 獲取下一個(gè)ID */ public long nextId() { // 如果當(dāng)前ID超過(guò)了分配范圍,則重新獲取一批 if (currentId.get() >= endId.get()) { lock.lock(); try { // 雙重檢查,防止多線程重復(fù)獲取 if (currentId.get() >= endId.get()) { // 執(zhí)行Lua腳本獲取一批ID Long newEndId = redisTemplate.execute( batchIncrScript, Collections.singletonList(ID_KEY), String.valueOf(BATCH_SIZE) ); if (newEndId == null) { throw new RuntimeException("Failed to generate batch ids"); } // 設(shè)置新的ID范圍 endId.set(newEndId); currentId.set(newEndId - BATCH_SIZE); } } finally { lock.unlock(); } } // 分配下一個(gè)ID return currentId.incrementAndGet(); } /** * 為指定業(yè)務(wù)生成ID */ public long nextId(String bizTag) { // 實(shí)際項(xiàng)目中應(yīng)該為每個(gè)業(yè)務(wù)標(biāo)簽維護(hù)獨(dú)立的計(jì)數(shù)器和范圍 // 這里簡(jiǎn)化處理,僅使用不同的Redis key String key = ID_KEY + ":" + bizTag; Long newEndId = redisTemplate.execute( batchIncrScript, Collections.singletonList(key), String.valueOf(1) ); return newEndId != null ? newEndId : -1; } }
優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 顯著減少Redis網(wǎng)絡(luò)請(qǐng)求次數(shù)
- 客戶端緩存ID段,大幅提高性能
- 降低Redis服務(wù)器壓力
- 支持突發(fā)流量處理
缺點(diǎn)
- 實(shí)現(xiàn)復(fù)雜度增加
- 服務(wù)重啟可能導(dǎo)致ID段浪費(fèi)
適用場(chǎng)景
- 高并發(fā)系統(tǒng),需要極高ID生成性能的場(chǎng)景
- 對(duì)ID連續(xù)性要求不嚴(yán)格的業(yè)務(wù)
- 能容忍小部分ID浪費(fèi)的場(chǎng)景
3. 基于Redis的分段式ID分配(號(hào)段模式)
原理
號(hào)段模式是一種優(yōu)化的批量ID生成方案,通過(guò)預(yù)分配號(hào)段(ID范圍)減少服務(wù)間競(jìng)爭(zhēng),同時(shí)引入雙Buffer機(jī)制提高可用性。
代碼實(shí)現(xiàn)
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Component public class RedisSegmentIdGenerator { private final RedisTemplate<String, String> redisTemplate; private final String SEGMENT_KEY = "distributed:segment:id"; private final DefaultRedisScript<Long> segmentScript; // 號(hào)段大小 private final int SEGMENT_STEP = 1000; // 加載因子,當(dāng)前號(hào)段使用到這個(gè)百分比時(shí)就異步加載下一個(gè)號(hào)段 private final double LOAD_FACTOR = 0.7; // 存儲(chǔ)業(yè)務(wù)號(hào)段信息的Map private final Map<String, SegmentBuffer> businessSegmentMap = new ConcurrentHashMap<>(); public RedisSegmentIdGenerator(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; // 創(chuàng)建Lua腳本 String scriptText = "local key = KEYS[1] " + "local step = tonumber(ARGV[1]) " + "local value = redis.call('incrby', key, step) " + "return value"; this.segmentScript = new DefaultRedisScript<>(); this.segmentScript.setScriptText(scriptText); this.segmentScript.setResultType(Long.class); } /** * 獲取下一個(gè)ID * @param bizTag 業(yè)務(wù)標(biāo)簽 * @return 唯一ID */ public long nextId(String bizTag) { // 獲取或創(chuàng)建號(hào)段緩沖區(qū) SegmentBuffer buffer = businessSegmentMap.computeIfAbsent( bizTag, k -> new SegmentBuffer(bizTag)); return buffer.nextId(); } /** * 內(nèi)部號(hào)段緩沖區(qū)類,實(shí)現(xiàn)雙Buffer機(jī)制 */ private class SegmentBuffer { private String bizTag; private Segment[] segments = new Segment[2]; // 雙Buffer private volatile int currentPos = 0; // 當(dāng)前使用的segment位置 private Lock lock = new ReentrantLock(); private volatile boolean isLoadingNext = false; // 是否正在異步加載下一個(gè)號(hào)段 public SegmentBuffer(String bizTag) { this.bizTag = bizTag; segments[0] = new Segment(0, 0); segments[1] = new Segment(0, 0); } /** * 獲取下一個(gè)ID */ public long nextId() { // 獲取當(dāng)前號(hào)段 Segment segment = segments[currentPos]; // 如果當(dāng)前號(hào)段為空或已用完,切換到另一個(gè)號(hào)段 if (!segment.isInitialized() || segment.getValue() > segment.getMax()) { lock.lock(); try { // 雙重檢查當(dāng)前號(hào)段狀態(tài) segment = segments[currentPos]; if (!segment.isInitialized() || segment.getValue() > segment.getMax()) { // 切換到另一個(gè)號(hào)段 currentPos = (currentPos + 1) % 2; segment = segments[currentPos]; // 如果另一個(gè)號(hào)段也未初始化或已用完,則同步加載 if (!segment.isInitialized() || segment.getValue() > segment.getMax()) { loadSegmentFromRedis(segment); } } } finally { lock.unlock(); } } // 檢查是否需要異步加載下一個(gè)號(hào)段 long value = segment.incrementAndGet(); if (value > segment.getMin() + (segment.getMax() - segment.getMin()) * LOAD_FACTOR && !isLoadingNext) { isLoadingNext = true; // 異步加載下一個(gè)號(hào)段 new Thread(() -> { Segment nextSegment = segments[(currentPos + 1) % 2]; loadSegmentFromRedis(nextSegment); isLoadingNext = false; }).start(); } return value; } /** * 從Redis加載號(hào)段 */ private void loadSegmentFromRedis(Segment segment) { String key = SEGMENT_KEY + ":" + bizTag; // 執(zhí)行Lua腳本獲取號(hào)段最大值 Long max = redisTemplate.execute( segmentScript, Collections.singletonList(key), String.valueOf(SEGMENT_STEP) ); if (max == null) { throw new RuntimeException("Failed to load segment from Redis"); } // 設(shè)置號(hào)段范圍 long min = max - SEGMENT_STEP + 1; segment.setMax(max); segment.setMin(min); segment.setValue(min - 1); // 設(shè)置為min-1,第一次incrementAndGet返回min segment.setInitialized(true); } } /** * 內(nèi)部號(hào)段類,存儲(chǔ)號(hào)段的范圍信息 */ private class Segment { private long min; // 最小值 private long max; // 最大值 private AtomicLong value; // 當(dāng)前值 private volatile boolean initialized; // 是否已初始化 public Segment(long min, long max) { this.min = min; this.max = max; this.value = new AtomicLong(min); this.initialized = false; } public long getValue() { return value.get(); } public void setValue(long value) { this.value.set(value); } public long incrementAndGet() { return value.incrementAndGet(); } public long getMin() { return min; } public void setMin(long min) { this.min = min; } public long getMax() { return max; } public void setMax(long max) { this.max = max; } public boolean isInitialized() { return initialized; } public void setInitialized(boolean initialized) { this.initialized = initialized; } } }
優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 雙Buffer設(shè)計(jì),高可用性
- 異步加載下一個(gè)號(hào)段,性能更高
- 大幅降低Redis訪問(wèn)頻率
- 即使Redis短暫不可用,仍可分配一段時(shí)間的ID
缺點(diǎn)
- 實(shí)現(xiàn)復(fù)雜,代碼量大
- 多實(shí)例部署時(shí),各實(shí)例獲取的號(hào)段不連續(xù)
- 重啟服務(wù)時(shí)號(hào)段內(nèi)的ID可能浪費(fèi)
- 需要在內(nèi)存中維護(hù)狀態(tài)
適用場(chǎng)景
- 對(duì)ID生成可用性要求高的業(yè)務(wù)
- 需要高性能且多服務(wù)器部署的分布式系統(tǒng)
4. 性能對(duì)比與選型建議
策略 | 性能 | 可用性 | ID長(zhǎng)度 | 實(shí)現(xiàn)復(fù)雜度 | 單調(diào)遞增 |
---|---|---|---|---|---|
INCR命令 | ★★★☆☆ | ★★☆☆☆ | 遞增整數(shù) | 低 | 嚴(yán)格遞增 |
Lua批量生成 | ★★★★★ | ★★★☆☆ | 遞增整數(shù) | 中 | 批次內(nèi)遞增 |
分段式ID | ★★★★★ | ★★★★☆ | 遞增整數(shù) | 高 | 段內(nèi)遞增 |
5. 實(shí)踐優(yōu)化技巧
1. Redis高可用配置
// 配置Redis哨兵模式,提高可用性 @Bean public RedisConnectionFactory redisConnectionFactory() { RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() .master("mymaster") .sentinel("127.0.0.1", 26379) .sentinel("127.0.0.1", 26380) .sentinel("127.0.0.1", 26381); return new LettuceConnectionFactory(sentinelConfig); }
2. ID預(yù)熱策略
// 系統(tǒng)啟動(dòng)時(shí)預(yù)熱ID生成器 @PostConstruct public void preWarmIdGenerator() { // 預(yù)先獲取一批ID,確保系統(tǒng)啟動(dòng)后立即可用 for (int i = 0; i < 10; i++) { try { segmentIdGenerator.nextId("order"); segmentIdGenerator.nextId("user"); segmentIdGenerator.nextId("payment"); } catch (Exception e) { log.error("Failed to pre-warm ID generator", e); } } }
3. 降級(jí)策略
// Redis不可用時(shí)的降級(jí)策略 public long nextIdWithFallback(String bizTag) { try { return segmentIdGenerator.nextId(bizTag); } catch (Exception e) { log.warn("Failed to get ID from Redis, using local fallback", e); // 使用本地UUID或其他替代方案 return Math.abs(UUID.randomUUID().getMostSignificantBits()); } }
6. 結(jié)論
選擇合適的分布式ID生成策略時(shí),需要綜合考慮系統(tǒng)規(guī)模、性能需求、可靠性要求和實(shí)現(xiàn)復(fù)雜度。無(wú)論選擇哪種方案,都應(yīng)注重高可用性設(shè)計(jì),增加監(jiān)控和預(yù)警機(jī)制,確保ID生成服務(wù)的穩(wěn)定運(yùn)行。
在實(shí)踐中,可以基于業(yè)務(wù)需求對(duì)這些方案進(jìn)行組合和優(yōu)化,例如為不同業(yè)務(wù)選擇不同策略,或者在ID中嵌入業(yè)務(wù)標(biāo)識(shí)等,打造更適合自身系統(tǒng)的分布式ID生成解決方案。
到此這篇關(guān)于基于Redis生成分布式全局唯一ID的3種策略的文章就介紹到這了,更多相關(guān)Redis生成分布式全局唯一ID內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java eclipse 整個(gè)項(xiàng)目或包查找只定字符串并替換操作
這篇文章主要介紹了java eclipse 整個(gè)項(xiàng)目或包查找只定字符串并替換操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09springboot整合redis過(guò)期key監(jiān)聽(tīng)實(shí)現(xiàn)訂單過(guò)期的項(xiàng)目實(shí)踐
現(xiàn)在各種電商平臺(tái)都有自己的訂單過(guò)期時(shí)間設(shè)置,那么如何設(shè)置訂單時(shí)間過(guò)期呢,本文主要介紹了springboot整合redis過(guò)期key監(jiān)聽(tīng)實(shí)現(xiàn)訂單過(guò)期的項(xiàng)目實(shí)踐,感興趣的可以了解一下2023-12-12如何使用兩個(gè)棧實(shí)現(xiàn)隊(duì)列Java
這篇文章主要介紹了如何使用兩個(gè)棧實(shí)現(xiàn)隊(duì)列Java,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11詳解使用Spring Boot開(kāi)發(fā)Web項(xiàng)目
這篇文章主要介紹了詳解使用Spring Boot開(kāi)發(fā)Web項(xiàng)目,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04