c#获取对象的唯一标识_在 Java 中利用 redis 实现分布式全局唯一标识服务
作者:?楊高超
juejin.im/post/5a4984265188252b145b643e
獲取全局唯一標(biāo)識(shí)的方法介紹
在一個(gè)IT系統(tǒng)中,獲取一個(gè)對(duì)象的唯一標(biāo)識(shí)符是一個(gè)普遍的需求。在以前的單體應(yīng)用中,如果數(shù)據(jù)庫是一個(gè)單數(shù)據(jù)庫的結(jié)構(gòu)。通常可以利用數(shù)據(jù)庫的自增字段來獲取這個(gè)唯一標(biāo)識(shí)。
例如,在 Mysql 數(shù)據(jù)庫中,我們可以通過 sql 語句創(chuàng)建一個(gè)自增長的 int 字段類型的表。如下所示
CREATE?TABLE?student
(
????id?INT?NOT?NULL?AUTO_INCREMENT,
????name?VARCHAR(16),
????PRIMARY?KEY?(id)
)
然后插入兩條數(shù)據(jù)
INSERT?INTO?student(name)?VALUE('yanggch');
INSERT?INTO?student(name)?VALUE('grace');
通過 SQL 語句查看表數(shù)據(jù)
?SELECT?*?FROM?student;
得到如下的結(jié)果
可以看到,雖然我們?cè)谕ㄟ^ SQL 插入數(shù)據(jù)的時(shí)候沒有指定 id 字段的值,但是因?yàn)樵撟侄蔚?AUTO_INCREMENT 自增長的特性,自動(dòng)的給兩條記錄添加了1和2兩個(gè)值。
這個(gè)方法有兩個(gè)主要問題。一個(gè)是如果是一個(gè)分庫分表的數(shù)據(jù)庫結(jié)構(gòu),那么在分布在不同實(shí)例中的同一個(gè)表中的id是重復(fù)的。另一個(gè)問題是記錄插入到數(shù)據(jù)庫里后,我們?cè)诖a中并不能知道剛剛插入數(shù)據(jù)庫的記錄的主鍵的值到底是什么。如果我們的一個(gè)業(yè)務(wù)是要同時(shí)插入一條主表記錄一節(jié)一系列以這條主表記錄主鍵為外鍵的子表記錄,我們?cè)诓迦胱颖碛涗浀臅r(shí)候,不知道對(duì)應(yīng)的外鍵的值是多少。導(dǎo)致無法插入。例如如果我們有一個(gè)下單業(yè)務(wù),要求在訂單表中插入一條訂單記錄,同時(shí)在訂單明細(xì)中插入多條在這個(gè)訂單中購買的商品的詳細(xì)信息的記錄。訂單數(shù)據(jù)插入成功后,我們不知道訂單的主鍵的值,所以我們也就無法正確的插入商品詳細(xì)信息記錄了。
另外一個(gè)利用數(shù)據(jù)庫自增字段屬性獲取唯一標(biāo)識(shí)方式是在數(shù)據(jù)庫中建立一個(gè)帶一個(gè)自增字段的數(shù)據(jù)表。每次在表中插入一條記錄,然后將這條記錄的值取出來作為主鍵值。這個(gè)的問題是每次要另外在數(shù)據(jù)表中插入一條記錄,同時(shí)在多用戶使用的環(huán)境下,要嚴(yán)格保證你取到的記錄就是你插入的記錄。否則會(huì)導(dǎo)致主鍵重復(fù)。著會(huì)讓獲取唯一標(biāo)識(shí)符的速度變得比較慢。同時(shí),這個(gè)方式在分庫分表的結(jié)構(gòu)下,也不能讓唯一標(biāo)識(shí)在全局唯一。
還有一些其他的方式。例如用 uuid 算法可以保證全局唯一,也能保證高性能。但是他生成是一個(gè)字符串,不能保證順序性,同時(shí)也太長了。
所以在分布式架構(gòu)中,我們就需要一個(gè)滿足如下條件的唯一標(biāo)識(shí)符服務(wù)
全局唯一
高性能
具備順序性
可以附加其他業(yè)務(wù)屬性
這里我們可以用 redis 的 INCR 命令來作為生成全局的唯一標(biāo)識(shí)符。INCR 命令的語法是
INCR?key
根據(jù) redis 的官網(wǎng)的 INCR 命令介紹,它是一個(gè)原子操作,效果是是將 redis 數(shù)據(jù)庫中 key 的值加一并且返回這個(gè)結(jié)果。如果 key 不存在,將在執(zhí)行加一操作前,將這個(gè) key 的值設(shè)置為0,也就是說執(zhí)行這個(gè)命令的結(jié)果是從 1 開始一直累加下去的。
同時(shí)我們可以看到這個(gè)命令的算法時(shí)間復(fù)雜度是 O(1),而 redis 的數(shù)據(jù)是存儲(chǔ)在內(nèi)存中的,這個(gè)命令的執(zhí)行速度是非常快的。在 redis 服務(wù)器為雙核 16g環(huán)境下,通過千兆局域網(wǎng)在另一臺(tái)服務(wù)器上命令行執(zhí)行壓力測試
redis-benchmark?-h?10.110.2.56?-p?52981?-a?hhSbcpotThgWdnxJNhrzwstSP20DvYOldkjf
結(jié)果如下
可以看到每秒可以生成5萬個(gè)標(biāo)識(shí)。這個(gè)可以滿足一般的高性能需求了
通過 Java 和 redis 實(shí)現(xiàn)一個(gè)全局唯一標(biāo)識(shí)服務(wù)
接下來我們來用繼續(xù)來在 Java 中利用 redis 來實(shí)現(xiàn)一個(gè)全局唯一標(biāo)識(shí)的服務(wù)。這個(gè)服務(wù)要滿足如下的需求
全局唯一
高性能
具備順序性
可以將日期數(shù)字作為全局唯一標(biāo)識(shí)的前綴
可以每天從 1 開始重新計(jì)數(shù)
不同的實(shí)體類型可以單獨(dú)生成標(biāo)識(shí)。例如訂單標(biāo)識(shí),會(huì)員標(biāo)識(shí)
可以在新的一天中從 1 開始計(jì)數(shù)
定義唯一標(biāo)識(shí)服務(wù)接口
package?com.x9710.common.redis;
/**
?*?全局唯一標(biāo)識(shí)服務(wù)接口
?*
?*?@author?楊高超
?*/
public?interface?UUIDService?{
/**
?*?每天從?1?開始生成唯一標(biāo)識(shí)
?*
?*?@param?key?????要生成唯一標(biāo)識(shí)的對(duì)象
?*?@param?length ?要生成為唯一標(biāo)識(shí)后綴的長度。不包括需要附加的時(shí)間前綴
?*????????????????如果?haveDay?=?false?或者?length?長度小于標(biāo)識(shí)后綴的長度則無效
?*?@param?haveDay?是否要附加日期前綴
?*?@return?唯一標(biāo)識(shí)
?*?@throws?Exception?異常
?*/
Long?fetchDailyUUID(String?key,?Integer?length,?Boolean?haveDay)?throws?Exception;
/**
?*?全局從?1?開始生成唯一標(biāo)識(shí)
?*
?*?@param?key?????要生成唯一標(biāo)識(shí)的對(duì)象
?*?@param?length ?要生成為唯一標(biāo)識(shí)后綴的長度。不包括需要附加的時(shí)間前綴
?*????????????????如果?haveDay?=?false?或者?length?長度小于標(biāo)識(shí)后綴的長度則無效
?*?@param?haveDay 是否要附加日期前綴。
?*?@return?唯一標(biāo)識(shí)
?*?@throws?Exception?異常
?*/
Long?fetchUUID(String?key,?Integer?length,?Boolean?haveDay)?throws?Exception;
}
基于 redis 實(shí)現(xiàn)唯一標(biāo)識(shí)服務(wù)
package?com.x9710.common.redis.impl;
import?com.x9710.common.redis.RedisConnection;
import?com.x9710.common.redis.UUIDService;
import?redis.clients.jedis.Jedis;
import?java.text.DateFormat;
import?java.text.NumberFormat;
import?java.text.SimpleDateFormat;
import?java.util.Calendar;
import?java.util.GregorianCalendar;
/**
?*?@author?楊高超
?*/
public?class?UUIDServiceRedisImpl?implements?UUIDService?{
private?RedisConnection?redisConnection;
private?Integer?dbIndex;
private?DateFormat?df?=?new?SimpleDateFormat("yyyyMMdd");
public?void?setRedisConnection(RedisConnection?redisConnection)?{
????this.redisConnection?=?redisConnection;
}
public?void?setDbIndex(Integer?dbIndex)?{
????this.dbIndex?=?dbIndex;
}
public?Long?fetchDailyUUID(String?key,?Integer?length,?Boolean?haveDay)?{
????Jedis?jedis?=?null;
????try?{
????????jedis?=?redisConnection.getJedis();
????????jedis.select(dbIndex);
????????Calendar?now?=?new?GregorianCalendar();
????????String?day?=?df.format(now.getTime());
????????//新的一天,通過新?key?獲取值,每天都能從1開始獲取
????????key?=?key?+?"_"?+?day;
????????Long?num?=?jedis.incr(key);
????????//設(shè)置?key?過期時(shí)間
????????if?(num?==?1)?{
????????????jedis.expire(key,?(24?-?now.get(Calendar.HOUR_OF_DAY))?*?3600?+?1800);
????????}
????????if?(haveDay)?{
????????????return?createUUID(num,?day,?length);
????????}?else?{
????????????return?num;
????????}
????}?finally?{
????????if?(jedis?!=?null)?{
????????????jedis.close();
????????}
????}
}
public?Long?fetchUUID(String?key,?Integer?length,?Boolean?haveDay)?{
????Jedis?jedis?=?null;
????try?{
????????jedis?=?redisConnection.getJedis();
????????jedis.select(dbIndex);
????????Calendar?now?=?new?GregorianCalendar();
????????Long?num?=?jedis.incr(key);
????????if?(haveDay)?{
????????????String?day?=?df.format(now.getTime());
????????????return?createUUID(num,?day,?length);
????????}?else?{
????????????return?num;
????????}
????}?finally?{
????????if?(jedis?!=?null)?{
????????????jedis.close();
????????}
????}
}
private?Long?createUUID(Long?num,?String?day,?Integer?length)?{
????String?id?=?String.valueOf(num);
????if?(id.length()?????????NumberFormat?nf?=?NumberFormat.getInstance();
????????nf.setGroupingUsed(false);
????????nf.setMaximumIntegerDigits(length);
????????nf.setMinimumIntegerDigits(length);
????????id?=?nf.format(num);
????}
????return?Long.parseLong(day?+?id);
}
}
編寫測試用例
在 Junit4 中不支持多線程測試,所以這里直接采用了 main 方法中運(yùn)行測試用例。
package?com.x9710.common.redis.test;
import?com.x9710.common.redis.RedisConnection;
import?com.x9710.common.redis.impl.UUIDServiceRedisImpl;
import?java.util.Date;
public?class?RedisUUIDTest?{
public?static?void?main(String[]?args)?{
????for?(int?i?=?0;?i?20;?i++)?{
????????new?Thread(new?Runnable()?{
????????????public?void?run()?{
????????????????RedisConnection?redisConnection?=?RedisConnectionUtil.create();
????????????????UUIDServiceRedisImpl?uuidServiceRedis?=?new?UUIDServiceRedisImpl();
????????????????uuidServiceRedis.setRedisConnection(redisConnection);
????????????????uuidServiceRedis.setDbIndex(15);
????????????????try?{
????????????????????for?(int?i?=?0;?i?100;?i++)?{
????????????????????????System.out.println(new?Date()?+?"?get?uuid?=?"?+?
??????????????????????????????uuidServiceRedis.fetchUUID("MEMBER",?8,?Boolean.TRUE)?+?
??????????????????????????????"?by?globle?in?"?+?Thread.currentThread().getName());
????????????????????}
????????????????}?catch?(Exception?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}
????????}).start();
????????new?Thread(new?Runnable()?{
????????????public?void?run()?{
????????????????RedisConnection?redisConnection?=?RedisConnectionUtil.create();
????????????????UUIDServiceRedisImpl?uuidServiceRedis?=?new?UUIDServiceRedisImpl();
????????????????uuidServiceRedis.setRedisConnection(redisConnection);
????????????????uuidServiceRedis.setDbIndex(15);
????????????????try?{
????????????????????for?(int?i?=?0;?i?100;?i++)?{
????????????????????????System.out.println(new?Date()?+?"?get?uuid?=?"?+?
????????????????????????????uuidServiceRedis.fetchDailyUUID("ORDER",?8,?Boolean.TRUE)?+?
????????????????????????????"?by?daily?in?"?+?Thread.currentThread().getName());
????????????????????}
????????????????}?catch?(Exception?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}
????????}).start();
????}
}
}
執(zhí)行結(jié)果如下
Mon?Dec?11?16:14:10?CST?2017?get?uuid?=?2017121100000003?by?member?in?Thread-32
Mon?Dec?11?16:14:10?CST?2017?get?uuid?=?2017121100000001?by?member?in?Thread-8
Mon?Dec?11?16:14:10?CST?2017?get?uuid?=?2017121100000007?by?order?in?Thread-19
......
Mon?Dec?11?16:14:14?CST?2017?get?uuid?=?2017121100002000?by?member?in?Thread-14
Mon?Dec?11?16:14:14?CST?2017?get?uuid?=?2017121100001999?by?member?in?Thread-16
Mon?Dec?11?16:14:14?CST?2017?get?uuid?=?2017121100001999?by?order?in?Thread-39
Mon?Dec?11?16:14:14?CST?2017?get?uuid?=?2017121100002000?by?order?in?Thread-39
這樣,我們就實(shí)現(xiàn)了一個(gè)滿足開始七個(gè)需求的一個(gè)基本的唯一標(biāo)識(shí)服務(wù)。只要調(diào)用這個(gè)模塊的程序連接的 redis 服務(wù)器的配置一樣,就能實(shí)現(xiàn)在同一個(gè)對(duì)象高效生成唯一標(biāo)識(shí)的基礎(chǔ)服務(wù)。你還可以將這個(gè)包裝成為一個(gè) rest 服務(wù),客戶端不需要直接連接 redis 服務(wù)器,直接通過 rest 的http 服務(wù)遠(yuǎn)程獲取唯一標(biāo)識(shí)即可。
GitHub地址
https://github.com/gaochao2000/redis_util
Java面試題專欄
【81期】面試官:說說HashMap 中的容量與擴(kuò)容實(shí)現(xiàn)
【82期】面試中被問到SQL優(yōu)化,看這篇就對(duì)了!
【83期】面試被問到了Redis和MongoDB的區(qū)別?看這里就對(duì)了
【84期】面試中設(shè)計(jì)模式能問些什么?比如說一下三種單例模式實(shí)現(xiàn)
【85期】談?wù)凧ava面向?qū)ο笤O(shè)計(jì)的六大原則,中高級(jí)面試常問!
【86期】五個(gè)刁鉆的String面試問題及解答
【87期】面試官問:Java序列化和反序列化為什么要實(shí)現(xiàn)Serializable接口
【88期】面試官問:你能說說 Spring 中,接口的bean是如何注入的嗎?
【89期】面試官 5 連問一個(gè) TCP 連接可以發(fā)多少個(gè) HTTP 請(qǐng)求?
【90期】面試官:說一下使用 Redis 實(shí)現(xiàn)大規(guī)模的帖子瀏覽計(jì)數(shù)的思路
歡迎長按下圖關(guān)注公眾號(hào)后端技術(shù)精選
總結(jié)
以上是生活随笔為你收集整理的c#获取对象的唯一标识_在 Java 中利用 redis 实现分布式全局唯一标识服务的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新手该怎么玩?
- 下一篇: cocos 卡牌类_优质链游大爆发,Co