基于Redis生成分布式全局唯一ID的3種策略
在分布式系統(tǒng)設計中,全局唯一ID是一個基礎而關鍵的組件。隨著業(yè)務規(guī)模擴大和系統(tǒng)架構向微服務演進,傳統(tǒng)的單機自增ID已無法滿足需求。高并發(fā)、高可用的分布式ID生成方案成為構建可靠分布式系統(tǒng)的必要條件。
Redis具備高性能、原子操作及簡單易用的特性,因此我們可以基于Redis實現(xiàn)全局唯一ID的生成。
分布式ID的核心需求
一個優(yōu)秀的分布式ID生成方案應滿足以下要求
- 全局唯一性:在整個分布式系統(tǒng)中保證ID不重復
- 高性能:能夠快速生成ID,支持高并發(fā)場景
- 高可用:避免單點故障,確保服務持續(xù)可用
- 趨勢遞增:生成的ID大致呈遞增趨勢,便于數(shù)據(jù)庫索引和分片
- 安全性(可選) :不包含敏感信息,不易被推測和偽造
1. 基于INCR命令的簡單自增ID
原理
這是最直接的Redis分布式ID實現(xiàn)方式,利用Redis的INCR命令原子性遞增一個計數(shù)器,確保在分布式環(huán)境下ID的唯一性。
代碼實現(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";
}
/**
* 生成下一個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è)務生成ID
* @param bizTag 業(yè)務標簽
* @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;
}
/**
* 獲取當前ID值但不遞增
* @param bizTag 業(yè)務標簽
* @return 當前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)缺點
優(yōu)點
- 實現(xiàn)極其簡單,僅需一次Redis操作
- ID嚴格遞增,適合作為數(shù)據(jù)庫主鍵
- 支持多業(yè)務ID隔離
缺點
- Redis單點故障會導致ID生成服務不可用
- 主從切換可能導致ID重復
- 無法包含業(yè)務含義
適用場景
- 中小規(guī)模系統(tǒng)的自增主鍵生成
- 對ID連續(xù)性有要求的業(yè)務場景
- 單數(shù)據(jù)中心部署的應用
2. 基于Lua腳本的批量ID生成
原理
通過Lua腳本一次性獲取一批ID,減少網(wǎng)絡往返次數(shù),客戶端可在內存中順序分配ID,顯著提高性能。
代碼實現(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;
// 本地計數(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);
}
/**
* 獲取下一個ID
*/
public long nextId() {
// 如果當前ID超過了分配范圍,則重新獲取一批
if (currentId.get() >= endId.get()) {
lock.lock();
try {
// 雙重檢查,防止多線程重復獲取
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");
}
// 設置新的ID范圍
endId.set(newEndId);
currentId.set(newEndId - BATCH_SIZE);
}
} finally {
lock.unlock();
}
}
// 分配下一個ID
return currentId.incrementAndGet();
}
/**
* 為指定業(yè)務生成ID
*/
public long nextId(String bizTag) {
// 實際項目中應該為每個業(yè)務標簽維護獨立的計數(shù)器和范圍
// 這里簡化處理,僅使用不同的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)缺點
優(yōu)點
- 顯著減少Redis網(wǎng)絡請求次數(shù)
- 客戶端緩存ID段,大幅提高性能
- 降低Redis服務器壓力
- 支持突發(fā)流量處理
缺點
- 實現(xiàn)復雜度增加
- 服務重啟可能導致ID段浪費
適用場景
- 高并發(fā)系統(tǒng),需要極高ID生成性能的場景
- 對ID連續(xù)性要求不嚴格的業(yè)務
- 能容忍小部分ID浪費的場景
3. 基于Redis的分段式ID分配(號段模式)
原理
號段模式是一種優(yōu)化的批量ID生成方案,通過預分配號段(ID范圍)減少服務間競爭,同時引入雙Buffer機制提高可用性。
代碼實現(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;
// 號段大小
private final int SEGMENT_STEP = 1000;
// 加載因子,當前號段使用到這個百分比時就異步加載下一個號段
private final double LOAD_FACTOR = 0.7;
// 存儲業(yè)務號段信息的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);
}
/**
* 獲取下一個ID
* @param bizTag 業(yè)務標簽
* @return 唯一ID
*/
public long nextId(String bizTag) {
// 獲取或創(chuàng)建號段緩沖區(qū)
SegmentBuffer buffer = businessSegmentMap.computeIfAbsent(
bizTag, k -> new SegmentBuffer(bizTag));
return buffer.nextId();
}
/**
* 內部號段緩沖區(qū)類,實現(xiàn)雙Buffer機制
*/
private class SegmentBuffer {
private String bizTag;
private Segment[] segments = new Segment[2]; // 雙Buffer
private volatile int currentPos = 0; // 當前使用的segment位置
private Lock lock = new ReentrantLock();
private volatile boolean isLoadingNext = false; // 是否正在異步加載下一個號段
public SegmentBuffer(String bizTag) {
this.bizTag = bizTag;
segments[0] = new Segment(0, 0);
segments[1] = new Segment(0, 0);
}
/**
* 獲取下一個ID
*/
public long nextId() {
// 獲取當前號段
Segment segment = segments[currentPos];
// 如果當前號段為空或已用完,切換到另一個號段
if (!segment.isInitialized() || segment.getValue() > segment.getMax()) {
lock.lock();
try {
// 雙重檢查當前號段狀態(tài)
segment = segments[currentPos];
if (!segment.isInitialized() || segment.getValue() > segment.getMax()) {
// 切換到另一個號段
currentPos = (currentPos + 1) % 2;
segment = segments[currentPos];
// 如果另一個號段也未初始化或已用完,則同步加載
if (!segment.isInitialized() || segment.getValue() > segment.getMax()) {
loadSegmentFromRedis(segment);
}
}
} finally {
lock.unlock();
}
}
// 檢查是否需要異步加載下一個號段
long value = segment.incrementAndGet();
if (value > segment.getMin() + (segment.getMax() - segment.getMin()) * LOAD_FACTOR
&& !isLoadingNext) {
isLoadingNext = true;
// 異步加載下一個號段
new Thread(() -> {
Segment nextSegment = segments[(currentPos + 1) % 2];
loadSegmentFromRedis(nextSegment);
isLoadingNext = false;
}).start();
}
return value;
}
/**
* 從Redis加載號段
*/
private void loadSegmentFromRedis(Segment segment) {
String key = SEGMENT_KEY + ":" + bizTag;
// 執(zhí)行Lua腳本獲取號段最大值
Long max = redisTemplate.execute(
segmentScript,
Collections.singletonList(key),
String.valueOf(SEGMENT_STEP)
);
if (max == null) {
throw new RuntimeException("Failed to load segment from Redis");
}
// 設置號段范圍
long min = max - SEGMENT_STEP + 1;
segment.setMax(max);
segment.setMin(min);
segment.setValue(min - 1); // 設置為min-1,第一次incrementAndGet返回min
segment.setInitialized(true);
}
}
/**
* 內部號段類,存儲號段的范圍信息
*/
private class Segment {
private long min; // 最小值
private long max; // 最大值
private AtomicLong value; // 當前值
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)缺點
優(yōu)點
- 雙Buffer設計,高可用性
- 異步加載下一個號段,性能更高
- 大幅降低Redis訪問頻率
- 即使Redis短暫不可用,仍可分配一段時間的ID
缺點
- 實現(xiàn)復雜,代碼量大
- 多實例部署時,各實例獲取的號段不連續(xù)
- 重啟服務時號段內的ID可能浪費
- 需要在內存中維護狀態(tài)
適用場景
- 對ID生成可用性要求高的業(yè)務
- 需要高性能且多服務器部署的分布式系統(tǒng)
4. 性能對比與選型建議
| 策略 | 性能 | 可用性 | ID長度 | 實現(xiàn)復雜度 | 單調遞增 |
|---|---|---|---|---|---|
| INCR命令 | ★★★☆☆ | ★★☆☆☆ | 遞增整數(shù) | 低 | 嚴格遞增 |
| Lua批量生成 | ★★★★★ | ★★★☆☆ | 遞增整數(shù) | 中 | 批次內遞增 |
| 分段式ID | ★★★★★ | ★★★★☆ | 遞增整數(shù) | 高 | 段內遞增 |
5. 實踐優(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預熱策略
// 系統(tǒng)啟動時預熱ID生成器
@PostConstruct
public void preWarmIdGenerator() {
// 預先獲取一批ID,確保系統(tǒ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. 降級策略
// Redis不可用時的降級策略
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. 結論
選擇合適的分布式ID生成策略時,需要綜合考慮系統(tǒng)規(guī)模、性能需求、可靠性要求和實現(xiàn)復雜度。無論選擇哪種方案,都應注重高可用性設計,增加監(jiān)控和預警機制,確保ID生成服務的穩(wěn)定運行。
在實踐中,可以基于業(yè)務需求對這些方案進行組合和優(yōu)化,例如為不同業(yè)務選擇不同策略,或者在ID中嵌入業(yè)務標識等,打造更適合自身系統(tǒng)的分布式ID生成解決方案。
到此這篇關于基于Redis生成分布式全局唯一ID的3種策略的文章就介紹到這了,更多相關Redis生成分布式全局唯一ID內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java獲取當前時間并轉化為yyyy-MM-dd?HH:mm:ss格式的多種方式
這篇文章主要介紹了Java獲取當前時間并轉化為yyyy-MM-dd?HH:mm:ss格式的多種方式,每種方式結合實例代碼給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-03-03
SSH框架網(wǎng)上商城項目第29戰(zhàn)之使用JsChart技術顯示商品銷售報表
這篇文章主要為大家詳細介紹了SSH框架網(wǎng)上商城項目第29戰(zhàn)之使用JsChart技術顯示商品銷售報表,感興趣的小伙伴們可以參考一下2016-06-06

