redis實(shí)現(xiàn)分布式全局唯一id的示例代碼
一、前言
在很多項(xiàng)目中生成類似訂單編號(hào)、用戶編號(hào)等有唯一性數(shù)據(jù)時(shí)還用的UUID工具,或者自己根據(jù)時(shí)間戳+隨機(jī)字符串等組合來(lái)生成,在并發(fā)小的時(shí)候很少出問(wèn)題,當(dāng)并發(fā)上來(lái)時(shí)就很可能出現(xiàn)重復(fù)編號(hào)的問(wèn)題了,單體項(xiàng)目和分布式項(xiàng)目都是如此,要想解決這個(gè)問(wèn)題也有很多種方法,可以自己寫一個(gè)唯一ID生成規(guī)則,也可以通過(guò)數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)全局ID生成這個(gè)和使用Redis實(shí)現(xiàn)其實(shí)類似,還可以使用比較成熟的雪花算法工具實(shí)現(xiàn),每種方法都有各自的優(yōu)缺點(diǎn)這里不展開說(shuō)明,這里詳細(xì)說(shuō)明如何使用Redis實(shí)現(xiàn)生成分布式全局唯一ID。
還有一個(gè)問(wèn)題為什么不能直接使用數(shù)據(jù)庫(kù)的自增ID,而是需要單獨(dú)生成一個(gè)分布式全局唯一ID,類似訂單IDON202311090001
,在數(shù)據(jù)庫(kù)中有自增ID,對(duì)于當(dāng)前業(yè)務(wù)來(lái)說(shuō)就是唯一的為什么不能用,還要去生成一個(gè)獨(dú)立的訂單ID,對(duì)于這個(gè)問(wèn)題要從幾個(gè)方面分析:
1、數(shù)據(jù)庫(kù)自增ID是有序增長(zhǎng)的很容易就被人猜到,比如我現(xiàn)在下一單看到的訂單ID為999那么就知道你的系統(tǒng)里最多只有999單,還有如果接口設(shè)計(jì)不合理,比如取消訂單接口只校驗(yàn)了用戶是否登錄沒(méi)有校驗(yàn)訂單是否屬于該用戶,接收一個(gè)訂單ID就能將訂單取消,那么這樣很容易就被人抓住漏洞,類似的情況有很多,也很多人寫接口是不會(huì)注意這個(gè)問(wèn)題。
2、這種自增ID沒(méi)有意義,而且不同業(yè)務(wù)的自增ID是重合的,對(duì)于信息區(qū)分度很低,而且考慮到多業(yè)務(wù)交互和用戶端展示也都是不合適的,想想看要是你在某寶下單,訂單ID是999,或者在對(duì)接別人訂單系統(tǒng)時(shí),給你的訂單ID是999是不是很奇怪。
3、分庫(kù)分表時(shí)自增ID會(huì)重復(fù)
全局ID生成器:是一種在【分布式系統(tǒng)下
】用來(lái)生成全局唯一ID的工具;
全局ID需要滿足的特性:
1.唯一性
2.高可用:集群、哨兵
機(jī)制;
3.高性能
4.遞增性:Redis中的String數(shù)據(jù)類型的有自增特性!
5.安全性:將自增數(shù)值進(jìn)行拼接,不容易猜出來(lái);
ID結(jié)構(gòu):符號(hào)位(1位) + 時(shí)間戳(31位)
+ 序列號(hào)(32位)
;
時(shí)間戳為從起始時(shí)間
到現(xiàn)在的時(shí)間差;
理論上支持1秒鐘2^32個(gè)訂單;
二、如何通過(guò)Redis設(shè)計(jì)一個(gè)分布式全局唯一ID生成工具
用戶下單調(diào)用下單邏輯,先進(jìn)行業(yè)務(wù)邏輯處理,然后攜帶訂單ID標(biāo)識(shí)通過(guò)分布式全局唯一ID工具獲取一個(gè)唯一的訂單ID,這個(gè)訂單ID標(biāo)識(shí)就是用于區(qū)分業(yè)務(wù)的,獲取到訂單ID后將數(shù)據(jù)組裝入庫(kù),分布式全局唯一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)行原子類的樂(lè)觀鎖操作,每一次獲取分布式全局唯一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
三、通過(guò)代碼實(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ù)都是有長(zhǎng)度限制的,或者是要求生成20字符的訂單編號(hào),如果增長(zhǎng)的過(guò)長(zhǎng)反而不好處理。
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ù)庫(kù)索引(默認(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ù)值表示沒(méi)有限制) max-active: 8 ## 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制) 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è)過(guò)期時(shí)間 if (!redisTemplate.hasKey(cacheKey)) { // 這里存在線程安全問(wèn)題,需要加分布式鎖,這里做簡(jiǎn)單實(shí)現(xiàn) String lockKey = cacheKey + "_LOCK"; // 設(shè)置分布式鎖 boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (!lock) { // 如果沒(méi)有拿到鎖進(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)在多線程的等待于喚醒問(wèn)題
我們?nèi)绻麤](méi)有CountDownLatch ,那么由于程序是異步的,當(dāng)異步程序沒(méi)有執(zhí)行完時(shí),主線程就已經(jīng)執(zhí)行完了,然后我們期望的是分線程全部走完之后,主線程再走,所以我們此時(shí)需要使用到CountDownLatch
CountDownLatch 中有兩個(gè)最重要的方法
countDown
await
await 方法 是阻塞方法,我們擔(dān)心分線程沒(méi)有執(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ì)出來(lái)的時(shí)間也就是所有分線程執(zhí)行完后的時(shí)間。
四、運(yùn)行結(jié)果
redis結(jié)果
代碼運(yùn)行結(jié)果,id沒(méi)有出現(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并發(fā)訪問(wèn)問(wèn)題詳細(xì)講解
本文主要介紹了Redis如何應(yīng)對(duì)并發(fā)訪問(wèn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-12-12redis和rabbitmq實(shí)現(xiàn)延時(shí)隊(duì)列的示例代碼
在高并發(fā)場(chǎng)景下,延遲隊(duì)列顯得尤為重要,本文主要介紹了兩種方式,redis和rabbitmq實(shí)現(xiàn)延時(shí)隊(duì)列,具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03Redis數(shù)據(jù)結(jié)構(gòu)SortedSet的底層原理解析
這篇文章主要介紹了Redis數(shù)據(jù)結(jié)構(gòu)SortedSet的底層原理解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07mac下redis安裝、設(shè)置、啟動(dòng)停止方法詳解
這篇文章主要介紹了mac下redis安裝、設(shè)置、啟動(dòng)停止方法詳解,需要的朋友可以參考下2020-02-02