在效能測試場景中,生成全域性唯一識別符號 (GUID) 是一個常見的需求,主要用於標識每個請求或者事務,以便於追蹤和分析。這是因為在效能測試中,需要對系統的各個功能進行測試,而每個功能都需要有一個唯一的標識來區分。如果不使用全域性唯一標識,則可能會出現重複標識的情況,導致測試結果不準確。
相信對於效能測試 er 來講這些並不陌生,特別在併發場景中使用各類的解決方案。我最近在研究 Go
語言執行緒安全問題的時候也被其他人問到了。所以打算單獨寫一寫唯一標識的主題,本來打算用一篇文章解決,但是在實踐中方案概述、方案實踐以及效能對比幾個部分,內容著實有點多。所以分成了上下兩篇,本篇講述幾種常見方案的概述和程式碼實踐,下一期我會分享幾種方案的效能。
UUID(Universally Unique Identifier)
UUID(通用唯一識別符號)是一種標準化的用於標識資訊的方法。通常用於分散式系統中的唯一標識,以防止不同系統中的資料重複或衝突。它在資料庫記錄、網路通訊、訊息佇列等方面都有廣泛的應用。它是由 128 位二進位制數表示的唯一識別符號,通常以 32 個十六進位制數字的形式表示,每四個數字之間用連字元分隔。UUID 的唯一性主要基於其隨機性和長度,儘管在某些情況下可能會出現重複,但重複的機率非常低。具體有多低呢,我查到資料是這麼說的:每秒產生10億筆UUID,100年後只產生一次重複的機率是50%。如果地球上每個人都各有6億筆GUID,發生一次重複的機率是50%。
。我暫時還沒遇到重複的情況,各位遇到請告訴我一下機率。
由於這是個自帶的包,可以使用java.util.UUID
類生成 UUID,例如:
UUID uuid = UUID.randomUUID();
String id = uuid.toString();
大概長這樣 245fee40-8b24-47d3-b5e1-09a5e48a08d1
。查閱資料過程中,還有多種版本的 UUID
,不知道是不是都這個格式。我用的 JDK17
,如果又不一樣格式的,興許版本不同導致的。
UUID 的優點包括:
- 全域性唯一性:UUID 基於其 128 位的長度和隨機性,可以在全球範圍內保證唯一性,極大地減少了資料衝突的可能性。
- 無序性:UUID 是無序的,不受時間和空間的限制,可以在任何地方、任何時間生成,不需要中心化管理。
- 高效能:生成 UUID 的速度非常快,幾乎可以瞬間完成,不會造成系統效能瓶頸。
- 不可推測性:UUID 是隨機生成的,不可預測,可以有效防止資訊被猜測或破解。
- 可擴充套件性:UUID 採用 128 位的長度,可以靈活地擴充套件應用範圍,適用於各種場景。
然而,UUID 也存在一些缺點:
- 長度較長:UUID 通常由 32 個十六進位制數字和四個連字元組成,總共 36 個字元,相比其他識別符號(如自增 ID)長度較長,佔用儲存空間較大。
- 不易讀:UUID 是一串十六進位制數字,對人類來說不夠友好,不如自增 ID 那樣直觀易讀。
- 不連續性:由於 UUID 是隨機生成的,所以其生成的順序是不連續的,不適合作為連續遞增的識別符號。
- 碰撞機率:雖然 UUID 的碰撞機率非常低,但隨著資料量的增加,碰撞的可能性也會增加,需要進行適當的處理和預防。
UUID 適用於需要全域性唯一標識且不依賴於中心化管理的場景,但在某些情況下可能會受到長度、可讀性和碰撞機率等因素的限制,需要根據具體情況進行選擇和權衡。如果我們在效能測試結束後清理資料的話,可以很大程式降低 UUID 重複的機率。
Redis/Zookeeper 等分散式服務生成 GUID
在分散式系統中,能夠生成全域性唯一 ID 是一個常見且重要的需求。全域性唯一 ID 不僅可以用於標識分佈在不同節點上的資料記錄,還可以用於追蹤分散式事務、訊息佇列等場景。傳統的基於資料庫自增序列或 UUID 等方式無法滿足分散式環境下的需求,因此需要藉助分散式服務來實現。
利用 Redis 的INCR
命令可以實現一個簡單的分散式 ID 生成器。Redis 是一個高效能的記憶體資料庫,它提供了原子操作命令INCR
用於對鍵值進行自增操作。我們可以在 Redis 中設定一個全域性的鍵,每次呼叫INCR
命令即可獲取一個唯一的 ID 值。由於 Redis 是單執行緒處理命令,因此可以確保獲取到的 ID 是全域性唯一的。這種方式實現簡單,但需要注意 Redis 的可用性和效能問題。
另一種方式是利用 Zookeeper
的有序臨時節點特性。Zookeeper 是一個分散式協調服務,它允許客戶端建立有序的臨時節點,節點名稱是一個遞增的計數器。我們可以在 Zookeeper 上建立一個根節點,每個客戶端在該節點下建立一個有序臨時節點,臨時節點的名稱就是一個全域性唯一的 ID。這種方式相對複雜,但可靠性和可用性更高,適合於關鍵任務型系統。
這種方式最大的缺點就是需要 N 多次的網路通訊,即使強如 Redis
也很難提供強大的效能,所以直接再次直接放棄了。對於效能要求不甚高的場景來說還是非常好用的。同樣地我在查閱資料中發現也有使用 MySQL
遞增主鍵實現的,效能就更差了,絕對不推薦。
雪花演算法
雪花演算法(Snowflake)是一種用於生成分散式系統中全域性唯一的 ID 的演算法。它由 Twitter 公司設計,採用了時間戳、機器 ID 和序列號等資訊,結合位運算的方式生成 64 位的唯一 ID。其中,時間戳部分用於保證 ID 的唯一性和遞增性,機器 ID 部分用於標識不同的機器,序列號部分用於解決同一毫秒內併發生成 ID 時的衝突。雪花演算法具有高效、高效能、高可用等特點,被廣泛應用於分散式系統中的 ID 生成。
雪花演算法很大程度上彌補了 UUID
的不足,而且使用非常靈活,幾十行程式碼即可完成,還能夠根據實際場景進行定製化,受到了越來越多碼農的喜歡。這裡我分享一個簡單的例子:
package com.funtester.utils;
public class SnowflakeUtils {
private static final long START_TIMESTAMP = 1616489534000L; // 起始時間戳,2021-03-23 00:00:00
private long datacenterId; // 資料中心ID
private long workerId; // 機器ID
private long sequence = 0L; // 序列號
private static final long MAX_WORKER_ID = 31L;// 機器ID最大值
private static final long MAX_DATA_CENTER_ID = 31L;// 資料中心ID最大值
private static final long SEQUENCE_BITS = 12L;// 序列號位數
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;// 機器ID左移位數
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_SHIFT;// 資料中心ID左移位數
private static final long TIMESTAMP_LEFT_SHIFT = DATA_CENTER_ID_SHIFT + DATA_CENTER_ID_SHIFT;// 時間戳左移位數
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 序列號掩碼
private long lastTimestamp = -1L;
public SnowflakeUtils(long datacenterId, long workerId) {
if (datacenterId > MAX_DATA_CENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("Datacenter ID can't be greater than " + MAX_DATA_CENTER_ID + " or less than 0");
}
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("Worker ID can't be greater than " + MAX_WORKER_ID + " or less than 0");
}
this.datacenterId = datacenterId;
this.workerId = workerId;
}
/**
* 獲取下一個ID
* * @return
*/
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = nextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
long l = ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT) | (datacenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence;
return l & Long.MAX_VALUE;
}
/**
* 獲取下一個時間戳
*
* @param lastTimestamp
* @return
*/
private long nextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
使用的方法如下:
public static void main(String[] args) {
SnowflakeUtils snowflake = new SnowflakeUtils(1, 1); // 建立雪花演算法例項,資料中心ID為1,機器ID為1
for (int i = 0; i < 5; i++) {
System.out.println("Next ID: " + snowflake.nextId());
}
}
結果大概長這個樣子:
Next ID: 3282842653393162240
Next ID: 3307893926320410624
Next ID: 3307893926320410625
Next ID: 3307893926320410626
Next ID: 3307893926320410627
我在 com.funtester.utils.SnowflakeUtils#nextId
方法的最後一行,加上了 l & Long.MAX_VALUE
為了獲取一個正的值。
執行緒獨享變數
在非併發場景當中,我們要想獲取一個全域性唯一的識別符號,最簡單的就是來一個 i++
,但這樣並不能保障併發場景中的執行緒安全。儘管如此,我們依舊可以透過之前分享過的 將共享變獨享
的思路改造一下,將每一個執行緒都分配一個 int i
,然後線上程內 i++
保障數值的唯一性。然後再給每一個執行緒進行唯一性標記,這個在之前分享執行緒工廠類時候提到過。如果遇到分散式場景,抄襲一下前面成熟框架的方法,增加唯一的機器碼標識即可。
下面是我使用的單機版本程式碼:
// 建立threadlocal物件
static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0
}
}
public static void main(String[] args) {
setPoolMax(3)
for (int i = 0; i < 10; i++) {
fun {
increase()// 增加1
System.out.println(Thread.currentThread().getName() + " threadLocal.get() = " + threadLocal.get());// 列印threadLocal值
}
}
}
/**
* 增加1
* @return
*/
static def increase() {
threadLocal.set(threadLocal.get() + 1)
}
輸出結果長這個樣子:
F-3 threadLocal.get() = 1
F-2 threadLocal.get() = 1
F-1 threadLocal.get() = 1
F-2 threadLocal.get() = 2
F-1 threadLocal.get() = 2
F-3 threadLocal.get() = 2
F-2 threadLocal.get() = 3
F-1 threadLocal.get() = 3
F-3 threadLocal.get() = 3
F-2 threadLocal.get() = 4
基本是實現了設計需求。缺點就是 java.lang.ThreadLocal
可能會導致記憶體溢位。這一點在效能測試當中可以忽略,因為用例執行完之後,JVM
自然也是要關閉的,如果是單 JVM
的效能測試服務,可以將 java.lang.ThreadLocal
物件設計成類成員屬性規避記憶體溢位的問題。
執行緒共享變數
這個思路就簡單了:新建一個全域性執行緒安全的變數,每次獲取一個值之後,安全地遞增 1,這樣一下子就解決了所有問題,是所有方案裡面最簡單使用的。方案的程式碼
演示程式碼如下:
// 定義全域性變數,用於執行緒安全遞增計數
static AtomicInteger index = new AtomicInteger(0)
public static void main(String[] args) {
setPoolMax(3)
for (int i = 0; i < 10; i++) {
fun {
println "遞增結果: ${index.incrementAndGet()}"
}
}
}
輸出結果:
遞增結果: 2
遞增結果: 3
遞增結果: 1
遞增結果: 4
遞增結果: 5
遞增結果: 6
遞增結果: 7
遞增結果: 8
遞增結果: 9
遞增結果: 10
相信個性化的方案不止一種,如果你也有一些有趣的方案,歡迎一起交流分享。
- 2021 年原創合集
- 2022 年原創合集
- 2023 年原創合集
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go、Python
- 單元&白盒&工具合集
- 測試方案&BUG&爬蟲&UI 自動化
- 測試理論雞湯
- 社群風采&影片合集