redis實(shí)現(xiàn)分布式全局唯一id的示例代碼
一、前言
在很多項(xiàng)目中生成類似訂單編號(hào)、用戶編號(hào)等有唯一性數(shù)據(jù)時(shí)還用的UUID工具,或者自己根據(jù)時(shí)間戳+隨機(jī)字符串等組合來生成,在并發(fā)小的時(shí)候很少出問題,當(dāng)并發(fā)上來時(shí)就很可能出現(xiàn)重復(fù)編號(hào)的問題了,單體項(xiàng)目和分布式項(xiàng)目都是如此,要想解決這個(gè)問題也有很多種方法,可以自己寫一個(gè)唯一ID生成規(guī)則,也可以通過數(shù)據(jù)庫來實(shí)現(xiàn)全局ID生成這個(gè)和使用Redis實(shí)現(xiàn)其實(shí)類似,還可以使用比較成熟的雪花算法工具實(shí)現(xiàn),每種方法都有各自的優(yōu)缺點(diǎn)這里不展開說明,這里詳細(xì)說明如何使用Redis實(shí)現(xiàn)生成分布式全局唯一ID。
還有一個(gè)問題為什么不能直接使用數(shù)據(jù)庫的自增ID,而是需要單獨(dú)生成一個(gè)分布式全局唯一ID,類似訂單IDON202311090001,在數(shù)據(jù)庫中有自增ID,對(duì)于當(dāng)前業(yè)務(wù)來說就是唯一的為什么不能用,還要去生成一個(gè)獨(dú)立的訂單ID,對(duì)于這個(gè)問題要從幾個(gè)方面分析:
1、數(shù)據(jù)庫自增ID是有序增長的很容易就被人猜到,比如我現(xiàn)在下一單看到的訂單ID為999那么就知道你的系統(tǒng)里最多只有999單,還有如果接口設(shè)計(jì)不合理,比如取消訂單接口只校驗(yàn)了用戶是否登錄沒有校驗(yàn)訂單是否屬于該用戶,接收一個(gè)訂單ID就能將訂單取消,那么這樣很容易就被人抓住漏洞,類似的情況有很多,也很多人寫接口是不會(huì)注意這個(gè)問題。
2、這種自增ID沒有意義,而且不同業(yè)務(wù)的自增ID是重合的,對(duì)于信息區(qū)分度很低,而且考慮到多業(yè)務(wù)交互和用戶端展示也都是不合適的,想想看要是你在某寶下單,訂單ID是999,或者在對(duì)接別人訂單系統(tǒng)時(shí),給你的訂單ID是999是不是很奇怪。
3、分庫分表時(shí)自增ID會(huì)重復(fù)
全局ID生成器:是一種在【分布式系統(tǒng)下】用來生成全局唯一ID的工具;
全局ID需要滿足的特性:
1.唯一性
2.高可用:集群、哨兵機(jī)制;
3.高性能
4.遞增性:Redis中的String數(shù)據(jù)類型的有自增特性!
5.安全性:將自增數(shù)值進(jìn)行拼接,不容易猜出來;
ID結(jié)構(gòu):符號(hào)位(1位) + 時(shí)間戳(31位) + 序列號(hào)(32位);
時(shí)間戳為從起始時(shí)間到現(xiàn)在的時(shí)間差;
理論上支持1秒鐘2^32個(gè)訂單;

二、如何通過Redis設(shè)計(jì)一個(gè)分布式全局唯一ID生成工具
用戶下單調(diào)用下單邏輯,先進(jìn)行業(yè)務(wù)邏輯處理,然后攜帶訂單ID標(biāo)識(shí)通過分布式全局唯一ID工具獲取一個(gè)唯一的訂單ID,這個(gè)訂單ID標(biāo)識(shí)就是用于區(qū)分業(yè)務(wù)的,獲取到訂單ID后將數(shù)據(jù)組裝入庫,分布式全局唯一ID工具可以做成一個(gè)內(nèi)嵌的utils,也可以封裝成一個(gè)獨(dú)立的jar,還可以做成一個(gè)分布式全局唯一ID生成服務(wù)供其它業(yè)務(wù)服務(wù)調(diào)用。

2.1 使用 Redis 計(jì)數(shù)器實(shí)現(xiàn)
Redis的String結(jié)構(gòu)提供了計(jì)數(shù)器自增功能,類似Java中的原子類,還要優(yōu)于Java的原子類,因?yàn)镽edis是單線程執(zhí)行的緩存讀寫本身就是線程安全的,也不用進(jìn)行原子類的樂觀鎖操作,每一次獲取分布式全局唯一ID時(shí)就將自增序列加1。
# 給key為GENERATEID:NO的value自增1,如果這key不存在則會(huì)添加到Redis中并且設(shè)置value為1 ## GENERATEID:key前綴 ## NO:訂單ID標(biāo)識(shí) 127.0.0.1:6379> incr GENERATEID:NO (integer) 1
2.2 使用 Redis Hash結(jié)構(gòu)實(shí)現(xiàn)
Redis Hash結(jié)構(gòu)中的每一個(gè)field也可以進(jìn)行自增操作,可以用一個(gè)Hash結(jié)構(gòu)存儲(chǔ)所有的標(biāo)識(shí)信息和自增序列,方便管理,比較適合并發(fā)不高的小項(xiàng)目所有服務(wù)都是用的一個(gè)Redis,如果并發(fā)較高就不合適了,畢竟Redis操作普通String結(jié)構(gòu)肯定比操作Hash結(jié)構(gòu)快。
# 給key為GENERATEID,field為no的value自增1,如果這key不存在則會(huì)添加到Redis中并且設(shè)置value為1 ## GENERATEID:分布式全局唯一ID Hash key ## NO:Hash結(jié)構(gòu)中的field 127.0.0.1:6379> hincrby GENERATEID NO 1 (integer) 1
三、通過代碼實(shí)現(xiàn)分布式全局唯一ID工具
這里使用Redis 計(jì)數(shù)器實(shí)現(xiàn),自增序列以天為單位存儲(chǔ),在實(shí)際業(yè)務(wù)中,比如生成訂單編號(hào)組成規(guī)則都類似NO1699631999000-1(業(yè)務(wù)標(biāo)識(shí)key+當(dāng)前時(shí)間戳+自增序列),這個(gè)規(guī)則可以自己定義,保證最終生成的訂單編號(hào)不重復(fù)即可,不建議直接一個(gè)自增序列干到底,訂單編號(hào)這類型的數(shù)據(jù)都是有長度限制的,或者是要求生成20字符的訂單編號(hào),如果增長的過長反而不好處理。
3.1 導(dǎo)入依賴配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
3.2 配置yml文件
spring:
#redis配置信息
redis:
## Redis數(shù)據(jù)庫索引(默認(rèn)為0)
database: 0
## Redis服務(wù)器地址
host: 127.0.0.1
## Redis服務(wù)器連接端口
port: 6379
## Redis服務(wù)器連接密碼(默認(rèn)為空)
password:
## 連接超時(shí)時(shí)間(毫秒)
timeout: 1200
lettuce:
pool:
## 連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
max-active: 8
## 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
max-wait: -1
## 連接池中的最大空閑連接
max-idle: 8
## 連接池中的最小空閑連接
min-idle: 1
3.3 序列化配置
@Configuration
public class RedisConfig {
//編寫我們自己的配置redisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// JSON序列化配置
Jackson2JsonRedisSerializer jsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper=new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jsonRedisSerializer.setObjectMapper(objectMapper);
// String的序列化
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
//key和hash的key都采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
//value和hash的value都采用jackson的序列化方式
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
3.4 編寫獲取工具
@Component
public class RedisGenerateIDUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// key前綴
private String PREFIX = "GENERATEID:";
/**
* 獲取全局唯一ID
* @param key 業(yè)務(wù)標(biāo)識(shí)key
*/
public String generateId(String key) {
// 獲取對(duì)應(yīng)業(yè)務(wù)自增序列
Long incr = getIncr(key);
// 組裝最后的結(jié)果,這里可以根據(jù)需要自己定義,這里是按照業(yè)務(wù)標(biāo)識(shí)key+當(dāng)前時(shí)間戳+自增序列進(jìn)行組裝
String resultID = key + System.currentTimeMillis() + "-" + incr;
return resultID;
}
/**
* 獲取對(duì)應(yīng)業(yè)務(wù)自增序列
*/
private Long getIncr(String key) {
String cacheKey = getCacheKey(key);
Long increment = 0L;
// 判斷Redis中是否存在這個(gè)自增序列,如果不存在添加一個(gè)序列并且設(shè)置一個(gè)過期時(shí)間
if (!redisTemplate.hasKey(cacheKey)) {
// 這里存在線程安全問題,需要加分布式鎖,這里做簡(jiǎn)單實(shí)現(xiàn)
String lockKey = cacheKey + "_LOCK";
// 設(shè)置分布式鎖
boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS);
if (!lock) {
// 如果沒有拿到鎖進(jìn)行自旋
return getIncr(key);
}
increment = redisTemplate.opsForValue().increment(cacheKey);
// 我這里設(shè)置24小時(shí),可以根據(jù)實(shí)際情況設(shè)置當(dāng)前時(shí)間到當(dāng)天結(jié)束時(shí)間的插值
redisTemplate.expire(cacheKey, 24, TimeUnit.HOURS);
// 釋放鎖
redisTemplate.delete(lockKey);
} else {
increment = redisTemplate.opsForValue().increment(cacheKey);
}
return increment;
}
/**
* 組裝緩存key
*/
private String getCacheKey(String key) {
return PREFIX + key + ":" + getYYYYMMDD();
}
/**
* 獲取當(dāng)前YYYYMMDD格式年月日
*/
private String getYYYYMMDD() {
LocalDate currentDate = LocalDate.now();
int year = currentDate.getYear();
int month = currentDate.getMonthValue();
int day = currentDate.getDayOfMonth();
return "" + year + month + day;
}
}
3.5 測(cè)試獲取工具
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisUniqueIdDemoApplication.class)
class RedisUniqueIdDemoApplicationTests {
@Resource
private RedisGenerateIDUtils redisGenerateIDUtils;
@Test
public void test() throws InterruptedException {
// 定義一個(gè)線程池 設(shè)置核心線程數(shù)和最大線程數(shù)都為100,隊(duì)列根據(jù)需要設(shè)置
ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 100, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10000));
CountDownLatch countDownLatch = new CountDownLatch(10000);
long beginTime = System.currentTimeMillis();
// 獲取10000個(gè)全局唯一ID 看看是否有重復(fù)
CopyOnWriteArraySet<String> ids = new CopyOnWriteArraySet<>();
for (int i = 0; i < 10000; i++) {
executor.execute(() -> {
// 獲取全局唯一ID
long beginTime02 = System.currentTimeMillis();
String orderNo = redisGenerateIDUtils.generateId("NO");
System.out.println(orderNo);
System.out.println("獲取單個(gè)ID耗時(shí) time=" + (System.currentTimeMillis() - beginTime02));
if (ids.contains(orderNo)) {
System.out.println("重復(fù)ID=" + orderNo);
} else {
ids.add(orderNo);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
// 打印獲取到的全局唯一ID集合數(shù)量
System.out.println("獲取到全局唯一ID count=" + ids.size());
System.out.println("耗時(shí)毫秒 time=" + (System.currentTimeMillis() - beginTime));
}
}
知識(shí)小貼士:關(guān)于countdownlatch
countdownlatch名為信號(hào)槍:主要的作用是同步協(xié)調(diào)在多線程的等待于喚醒問題
我們?nèi)绻麤]有CountDownLatch ,那么由于程序是異步的,當(dāng)異步程序沒有執(zhí)行完時(shí),主線程就已經(jīng)執(zhí)行完了,然后我們期望的是分線程全部走完之后,主線程再走,所以我們此時(shí)需要使用到CountDownLatch
CountDownLatch 中有兩個(gè)最重要的方法
countDown
await
await 方法 是阻塞方法,我們擔(dān)心分線程沒有執(zhí)行完時(shí),main線程就先執(zhí)行,所以使用await可以讓main線程阻塞,那么什么時(shí)候main線程不再阻塞呢?當(dāng)CountDownLatch 內(nèi)部維護(hù)的 變量變?yōu)?時(shí),就不再阻塞,直接放行,那么什么時(shí)候CountDownLatch 維護(hù)的變量變?yōu)? 呢,我們只需要調(diào)用一次countDown ,內(nèi)部變量就減少1,我們讓分線程和變量綁定, 執(zhí)行完一個(gè)分線程就減少一個(gè)變量,當(dāng)分線程全部走完,CountDownLatch 維護(hù)的變量就是0,此時(shí)await就不再阻塞,統(tǒng)計(jì)出來的時(shí)間也就是所有分線程執(zhí)行完后的時(shí)間。
四、運(yùn)行結(jié)果
redis結(jié)果

代碼運(yùn)行結(jié)果,id沒有出現(xiàn)重復(fù):

代碼地址:Github
到此這篇關(guān)于redis實(shí)現(xiàn)分布式全局唯一id的示例代碼的文章就介紹到這了,更多相關(guān)redis 分布式全局唯一id內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis動(dòng)態(tài)熱點(diǎn)數(shù)據(jù)緩存策略設(shè)計(jì)
本文主要介紹了Redis動(dòng)態(tài)熱點(diǎn)數(shù)據(jù)緩存策略設(shè)計(jì),包括熱點(diǎn)數(shù)據(jù)識(shí)別、動(dòng)態(tài)緩存、多級(jí)緩存、預(yù)加載機(jī)制、更新策略以及監(jiān)控告警等,具有一定的參考價(jià)值,感興趣的可以了解一下2025-01-01
Redis分布式限流組件設(shè)計(jì)與使用實(shí)例
本文主要講解基于 自定義注解+Aop+反射+Redis+Lua表達(dá)式 實(shí)現(xiàn)的限流設(shè)計(jì)方案。實(shí)現(xiàn)的限流設(shè)計(jì)與實(shí)際使用。具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Redis哨兵主備切換的數(shù)據(jù)丟失問題及解決
主備切換過程中可能會(huì)導(dǎo)致數(shù)據(jù)丟失,異步復(fù)制和腦裂是兩種主要原因,異步復(fù)制可能導(dǎo)致部分?jǐn)?shù)據(jù)未復(fù)制到slave而master宕機(jī),腦裂則可能導(dǎo)致多個(gè)master存在,舊master恢復(fù)后數(shù)據(jù)被清空,從而丟失數(shù)據(jù)2024-12-12
gem install redis報(bào)錯(cuò)的解決方案
今天小編就為大家分享一篇關(guān)于gem install redis報(bào)錯(cuò)的解決方案,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01
Redis字符串String操作詳解從基礎(chǔ)到高級(jí)應(yīng)用小結(jié)
本文全面解析Redis字符串類型,涵蓋設(shè)置、修改、數(shù)字運(yùn)算、過期處理及分布式鎖、計(jì)數(shù)器等高級(jí)應(yīng)用,提供基礎(chǔ)命令與性能優(yōu)化策略,助開發(fā)者高效利用這一核心數(shù)據(jù)類型實(shí)現(xiàn)緩存、統(tǒng)計(jì)等功能,感興趣的朋友一起看看吧2025-07-07
Redis+aop實(shí)現(xiàn)接口防刷(冪等)的解決方案
在高并發(fā)場(chǎng)景下,可能會(huì)因?yàn)榫W(wǎng)絡(luò)或者服務(wù)器原因,造成延遲,同時(shí)就是有可能會(huì)有人用腳本大量訪問你的接口,造成資源崩潰,所以本文給大家介紹了Redis+aop實(shí)現(xiàn)接口防刷(冪等)的解決方案,需要的朋友可以參考下2024-03-03

