c#获取对象的唯一标识_在 Java 中利用 redis 实现分布式全局唯一标识服务
作者:?楊高超
juejin.im/post/5a4984265188252b145b643e
獲取全局唯一標識的方法介紹
在一個IT系統中,獲取一個對象的唯一標識符是一個普遍的需求。在以前的單體應用中,如果數據庫是一個單數據庫的結構。通常可以利用數據庫的自增字段來獲取這個唯一標識。
例如,在 Mysql 數據庫中,我們可以通過 sql 語句創建一個自增長的 int 字段類型的表。如下所示
CREATE?TABLE?student
(
????id?INT?NOT?NULL?AUTO_INCREMENT,
????name?VARCHAR(16),
????PRIMARY?KEY?(id)
)
然后插入兩條數據
INSERT?INTO?student(name)?VALUE('yanggch');
INSERT?INTO?student(name)?VALUE('grace');
通過 SQL 語句查看表數據
?SELECT?*?FROM?student;
得到如下的結果
可以看到,雖然我們在通過 SQL 插入數據的時候沒有指定 id 字段的值,但是因為該字段的 AUTO_INCREMENT 自增長的特性,自動的給兩條記錄添加了1和2兩個值。
這個方法有兩個主要問題。一個是如果是一個分庫分表的數據庫結構,那么在分布在不同實例中的同一個表中的id是重復的。另一個問題是記錄插入到數據庫里后,我們在代碼中并不能知道剛剛插入數據庫的記錄的主鍵的值到底是什么。如果我們的一個業務是要同時插入一條主表記錄一節一系列以這條主表記錄主鍵為外鍵的子表記錄,我們在插入子表記錄的時候,不知道對應的外鍵的值是多少。導致無法插入。例如如果我們有一個下單業務,要求在訂單表中插入一條訂單記錄,同時在訂單明細中插入多條在這個訂單中購買的商品的詳細信息的記錄。訂單數據插入成功后,我們不知道訂單的主鍵的值,所以我們也就無法正確的插入商品詳細信息記錄了。
另外一個利用數據庫自增字段屬性獲取唯一標識方式是在數據庫中建立一個帶一個自增字段的數據表。每次在表中插入一條記錄,然后將這條記錄的值取出來作為主鍵值。這個的問題是每次要另外在數據表中插入一條記錄,同時在多用戶使用的環境下,要嚴格保證你取到的記錄就是你插入的記錄。否則會導致主鍵重復。著會讓獲取唯一標識符的速度變得比較慢。同時,這個方式在分庫分表的結構下,也不能讓唯一標識在全局唯一。
還有一些其他的方式。例如用 uuid 算法可以保證全局唯一,也能保證高性能。但是他生成是一個字符串,不能保證順序性,同時也太長了。
所以在分布式架構中,我們就需要一個滿足如下條件的唯一標識符服務
全局唯一
高性能
具備順序性
可以附加其他業務屬性
這里我們可以用 redis 的 INCR 命令來作為生成全局的唯一標識符。INCR 命令的語法是
INCR?key
根據 redis 的官網的 INCR 命令介紹,它是一個原子操作,效果是是將 redis 數據庫中 key 的值加一并且返回這個結果。如果 key 不存在,將在執行加一操作前,將這個 key 的值設置為0,也就是說執行這個命令的結果是從 1 開始一直累加下去的。
同時我們可以看到這個命令的算法時間復雜度是 O(1),而 redis 的數據是存儲在內存中的,這個命令的執行速度是非常快的。在 redis 服務器為雙核 16g環境下,通過千兆局域網在另一臺服務器上命令行執行壓力測試
redis-benchmark?-h?10.110.2.56?-p?52981?-a?hhSbcpotThgWdnxJNhrzwstSP20DvYOldkjf
結果如下
可以看到每秒可以生成5萬個標識。這個可以滿足一般的高性能需求了
通過 Java 和 redis 實現一個全局唯一標識服務
接下來我們來用繼續來在 Java 中利用 redis 來實現一個全局唯一標識的服務。這個服務要滿足如下的需求
全局唯一
高性能
具備順序性
可以將日期數字作為全局唯一標識的前綴
可以每天從 1 開始重新計數
不同的實體類型可以單獨生成標識。例如訂單標識,會員標識
可以在新的一天中從 1 開始計數
定義唯一標識服務接口
package?com.x9710.common.redis;
/**
?*?全局唯一標識服務接口
?*
?*?@author?楊高超
?*/
public?interface?UUIDService?{
/**
?*?每天從?1?開始生成唯一標識
?*
?*?@param?key?????要生成唯一標識的對象
?*?@param?length ?要生成為唯一標識后綴的長度。不包括需要附加的時間前綴
?*????????????????如果?haveDay?=?false?或者?length?長度小于標識后綴的長度則無效
?*?@param?haveDay?是否要附加日期前綴
?*?@return?唯一標識
?*?@throws?Exception?異常
?*/
Long?fetchDailyUUID(String?key,?Integer?length,?Boolean?haveDay)?throws?Exception;
/**
?*?全局從?1?開始生成唯一標識
?*
?*?@param?key?????要生成唯一標識的對象
?*?@param?length ?要生成為唯一標識后綴的長度。不包括需要附加的時間前綴
?*????????????????如果?haveDay?=?false?或者?length?長度小于標識后綴的長度則無效
?*?@param?haveDay 是否要附加日期前綴。
?*?@return?唯一標識
?*?@throws?Exception?異常
?*/
Long?fetchUUID(String?key,?Integer?length,?Boolean?haveDay)?throws?Exception;
}
基于 redis 實現唯一標識服務
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);
????????//設置?key?過期時間
????????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 方法中運行測試用例。
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();
????}
}
}
執行結果如下
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
這樣,我們就實現了一個滿足開始七個需求的一個基本的唯一標識服務。只要調用這個模塊的程序連接的 redis 服務器的配置一樣,就能實現在同一個對象高效生成唯一標識的基礎服務。你還可以將這個包裝成為一個 rest 服務,客戶端不需要直接連接 redis 服務器,直接通過 rest 的http 服務遠程獲取唯一標識即可。
GitHub地址
https://github.com/gaochao2000/redis_util
Java面試題專欄
【81期】面試官:說說HashMap 中的容量與擴容實現
【82期】面試中被問到SQL優化,看這篇就對了!
【83期】面試被問到了Redis和MongoDB的區別?看這里就對了
【84期】面試中設計模式能問些什么?比如說一下三種單例模式實現
【85期】談談Java面向對象設計的六大原則,中高級面試常問!
【86期】五個刁鉆的String面試問題及解答
【87期】面試官問:Java序列化和反序列化為什么要實現Serializable接口
【88期】面試官問:你能說說 Spring 中,接口的bean是如何注入的嗎?
【89期】面試官 5 連問一個 TCP 連接可以發多少個 HTTP 請求?
【90期】面試官:說一下使用 Redis 實現大規模的帖子瀏覽計數的思路
歡迎長按下圖關注公眾號后端技術精選
總結
以上是生活随笔為你收集整理的c#获取对象的唯一标识_在 Java 中利用 redis 实现分布式全局唯一标识服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新手该怎么玩?
- 下一篇: 阿拉伯语qq网名大全