收藏的網(wǎng)站從做系統(tǒng)后找不到了東莞關(guān)鍵詞優(yōu)化推廣
文章目錄
- 前言
- 1. 自增ID(Auto-Increment)
- 2. GUID (Globally Unique Identifier)
- 3. 雪花算法(Snowflake)
- 處理時鐘回撥的方法
- 1. 簡單等待
- 2. 配置時鐘回撥安全窗口
- 3. 使用不同的機器 ID
- 小結(jié)
- 穩(wěn)定的雪花算法實現(xiàn)方案
- 示例實現(xiàn)
- 1. 定義雪花算法類
- 2. 使用 Redis 或數(shù)據(jù)庫實現(xiàn)分布式唯一 ID
- 解釋
- 小結(jié)
- 其他方法
- 總結(jié)
前言
數(shù)據(jù)庫主鍵的設計是數(shù)據(jù)庫架構(gòu)中的一個重要環(huán)節(jié),不同的主鍵生成策略適用于不同的場景和需求
以下是幾種常見的主鍵設計方法及其優(yōu)缺點比較:
1. 自增ID(Auto-Increment)
優(yōu)點:
- 實現(xiàn)簡單,數(shù)據(jù)庫自動管理,無需開發(fā)者介入。
- 遞增的特性使得數(shù)據(jù)插入速度快,因為插入總是發(fā)生在索引的末尾。
- 易于理解和使用,便于查詢和排序。
缺點:
- 分布式系統(tǒng)中難以保證全局唯一,因為每個節(jié)點的計數(shù)器獨立增長。
- 數(shù)據(jù)泄露風險,自增ID容易暴露數(shù)據(jù)庫的規(guī)模和增長速度。
- 如果發(fā)生大量刪除操作,可能導致主鍵ID不連續(xù),影響美觀但不影響功能。
2. GUID (Globally Unique Identifier)
優(yōu)點:
- 全球唯一,無論在任何系統(tǒng)、任何地點生成,都能保證唯一性。
- 無需依賴數(shù)據(jù)庫,可以在客戶端生成,適合分布式系統(tǒng)。
- 支持提前生成ID,有利于并行處理和離線操作。
缺點:
- 長度較大(通常為32字符),占用更多的存儲空間和索引空間。
- 無序的特性可能導致索引碎片,降低插入性能。
- 不易讀,不便于人工識別和調(diào)試。
3. 雪花算法(Snowflake)
雪花算法(Snowflake Algorithm)是一種用于生成唯一ID的算法,最初由Twitter公司開發(fā)。它是為了解決分布式系統(tǒng)中生成全局唯一ID的需求而設計的。在分布式系統(tǒng)中,如果不同節(jié)點生成的ID可能會發(fā)生沖突,這就需要一種機制來保證生成的ID在整個系統(tǒng)中唯一。
雪花算法的設計考慮了以下幾個因素:
- 時間戳(Timestamp):使用當前時間來確保生成的ID是遞增的,這樣可以保證生成的ID是有序的。
- 機器ID(Machine ID):將機器的唯一標識(比如機器的MAC地址)作為一部分ID,確保不同機器生成的ID不會沖突。
- 序列號(Sequence Number):用來解決同一毫秒內(nèi)生成多個ID時的沖突問題。
Java中如何使用雪花算法來設計數(shù)據(jù)庫主鍵呢?下面是一個簡單的示例:
public class SnowflakeIdGenerator {// 定義機器ID,可以通過配置文件或其他方式設置private long machineId;// 定義序列號private long sequence = 0L;// 定義初始時間戳private long twepoch = 1622874000000L; // 2021-06-05 00:00:00// 定義各部分占位數(shù)private long machineIdBits = 5L;private long maxMachineId = -1L ^ (-1L << machineIdBits);private long sequenceBits = 12L;private long sequenceMask = -1L ^ (-1L << sequenceBits);// 定義機器ID左移位數(shù)private long machineIdShift = sequenceBits;// 定義時間戳左移位數(shù)private long timestampLeftShift = sequenceBits + machineIdBits;// 上次生成ID的時間戳private long lastTimestamp = -1L;public SnowflakeIdGenerator(long machineId) {if (machineId > maxMachineId || machineId < 0) {throw new IllegalArgumentException("Machine ID can't be greater than " + maxMachineId + " or less than 0");}this.machineId = machineId;}public synchronized long nextId() {long timestamp = timeGen();if (timestamp < lastTimestamp) {throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");}if (lastTimestamp == timestamp) {sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {// 當同一毫秒內(nèi)的序列號超過上限時,等待下一毫秒timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = timestamp;return ((timestamp - twepoch) << timestampLeftShift) | (machineId << machineIdShift) | sequence;}private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}private long timeGen() {return System.currentTimeMillis();}public static void main(String[] args) {SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1); // 傳入機器IDfor (int i = 0; i < 10; i++) {System.out.println(idGenerator.nextId());}}
}
在這個示例中,我們通過nextId()
方法來生成雪花算法生成的唯一ID。首先,我們需要設置一個機器ID,確保不同的機器有不同的ID。然后,調(diào)用nextId()
方法即可生成一個唯一的ID,這個ID包含了時間戳、機器ID和序列號三部分。最后,我們可以將生成的ID作為數(shù)據(jù)庫表的主鍵。
值得注意的是,雪花算法生成的ID是趨勢遞增的,因此在數(shù)據(jù)庫中使用時可能會帶來一定的優(yōu)勢,比如輔助索引的性能優(yōu)化。但也要注意在高并發(fā)情況下可能出現(xiàn)的一些問題,比如時鐘回撥等。
優(yōu)點:
- 結(jié)合了自增ID和GUID的優(yōu)點,生成的ID是趨勢遞增的,且全局唯一。
- 高性能,適用于分布式環(huán)境,能夠按需分配workerId和數(shù)據(jù)中心id,保證唯一性。
- ID較短(一般為64位),相比GUID節(jié)省存儲空間。
- 有序性有助于索引優(yōu)化。
缺點:
- 需要一個中心節(jié)點(或者多個,但需要協(xié)調(diào))來生成ID,有一定的運維成本。
- 時鐘回撥問題可能會影響ID的生成,需要特殊處理。
然而,雪花算法依賴于時間戳,因此時鐘回撥(clock rollback)會對其造成問題。
處理時鐘回撥的方法
1. 簡單等待
當檢測到時鐘回撥時,直接等待直到時間回到正確的時間。這是最簡單的處理方式,但會導致 ID 生成暫停一段時間。
public class SnowflakeIdGenerator {private long lastTimestamp = -1L;public synchronized long nextId() {long timestamp = timeGen();// 如果當前時間小于上一次生成ID的時間戳,說明系統(tǒng)時鐘回撥if (timestamp < lastTimestamp) {// 等待直到時鐘追上while (timestamp < lastTimestamp) {timestamp = timeGen();}}lastTimestamp = timestamp;return generateId(timestamp);}private long timeGen() {return System.currentTimeMillis();}private long generateId(long timestamp) {// 生成ID的邏輯return timestamp;}
}
2. 配置時鐘回撥安全窗口
允許一定范圍內(nèi)的時鐘回撥,在這個范圍內(nèi)繼續(xù)生成 ID,但如果超出這個范圍則拋出異?;虿扇∑渌胧?/p>
public class SnowflakeIdGenerator {private long lastTimestamp = -1L;private static final long MAX_BACKWARD_MS = 5L; // 允許的最大時鐘回撥時間public synchronized long nextId() {long timestamp = timeGen();if (timestamp < lastTimestamp) {long offset = lastTimestamp - timestamp;if (offset <= MAX_BACKWARD_MS) {// 等待,直到時鐘追上try {Thread.sleep(offset + 1);} catch (InterruptedException e) {throw new RuntimeException(e);}timestamp = timeGen();if (timestamp < lastTimestamp) {throw new RuntimeException("Clock moved backwards. Refusing to generate id");}} else {throw new RuntimeException("Clock moved backwards. Refusing to generate id");}}lastTimestamp = timestamp;return generateId(timestamp);}private long timeGen() {return System.currentTimeMillis();}private long generateId(long timestamp) {// 生成ID的邏輯return timestamp;}
}
3. 使用不同的機器 ID
在分布式系統(tǒng)中,每臺機器有唯一的機器 ID。當檢測到時鐘回撥時,改變機器 ID 來避免沖突。這種方法需要協(xié)調(diào)機器 ID 的分配。
public class SnowflakeIdGenerator {private long lastTimestamp = -1L;private long machineId;private static final long MAX_MACHINE_ID = 1023L;public SnowflakeIdGenerator(long machineId) {if (machineId < 0 || machineId > MAX_MACHINE_ID) {throw new IllegalArgumentException("Machine ID out of range");}this.machineId = machineId;}public synchronized long nextId() {long timestamp = timeGen();if (timestamp < lastTimestamp) {machineId = (machineId + 1) & MAX_MACHINE_ID;if (machineId == 0) {// 如果機器ID回到0,說明時鐘回撥過大,拒絕生成IDthrow new RuntimeException("Clock moved backwards. Refusing to generate id");}timestamp = timeGen();}lastTimestamp = timestamp;return generateId(timestamp, machineId);}private long timeGen() {return System.currentTimeMillis();}private long generateId(long timestamp, long machineId) {// 生成ID的邏輯,包含時間戳和機器IDreturn (timestamp << 22) | (machineId << 12);}
}
小結(jié)
- 簡單等待:當檢測到時鐘回撥時,等待直到時鐘恢復到正確時間。這種方法簡單但會導致 ID 生成暫停。
- 時鐘回撥安全窗口:允許一定范圍內(nèi)的時鐘回撥,如果超出這個范圍則拋出異?;虿扇∑渌胧?。
- 不同的機器 ID:當檢測到時鐘回撥時,改變機器 ID 來避免沖突。這種方法需要協(xié)調(diào)機器 ID 的分配。
穩(wěn)定的雪花算法實現(xiàn)方案
以下是一個經(jīng)過優(yōu)化的方案,涵蓋時鐘回撥問題、分布式系統(tǒng)中的唯一性問題和高可用性問題
- 機器 ID 和數(shù)據(jù)中心 ID:通過配置不同的機器 ID 和數(shù)據(jù)中心 ID 來確保分布式系統(tǒng)中的唯一性。
- 時鐘回撥處理:使用遞增序列和緩存的時間戳來處理時鐘回撥問題。
- 高可用性:結(jié)合 Redis 或數(shù)據(jù)庫來生成分布式唯一 ID。
示例實現(xiàn)
1. 定義雪花算法類
public class SnowflakeIdGenerator {private static final long EPOCH = 1609459200000L; // 自定義紀元時間(2021-01-01)private static final long DATA_CENTER_ID_BITS = 5L;private static final long MACHINE_ID_BITS = 5L;private static final long SEQUENCE_BITS = 12L;private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATA_CENTER_ID_BITS;private final long dataCenterId;private final long machineId;private long sequence = 0L;private long lastTimestamp = -1L;public SnowflakeIdGenerator(long dataCenterId, long machineId) {if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {throw new IllegalArgumentException(String.format("DataCenter ID can't be greater than %d or less than 0", MAX_DATA_CENTER_ID));}if (machineId > MAX_MACHINE_ID || machineId < 0) {throw new IllegalArgumentException(String.format("Machine ID can't be greater than %d or less than 0", MAX_MACHINE_ID));}this.dataCenterId = dataCenterId;this.machineId = machineId;}public synchronized long nextId() {long timestamp = timeGen();if (timestamp < lastTimestamp) {throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");}if (timestamp == lastTimestamp) {sequence = (sequence + 1) & MAX_SEQUENCE;if (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = timestamp;return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)| (dataCenterId << DATA_CENTER_ID_SHIFT)| (machineId << MACHINE_ID_SHIFT)| sequence;}private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}private long timeGen() {return System.currentTimeMillis();}
}
2. 使用 Redis 或數(shù)據(jù)庫實現(xiàn)分布式唯一 ID
為了進一步提高高可用性和唯一性,可以結(jié)合 Redis 或數(shù)據(jù)庫實現(xiàn)分布式唯一 ID 生成。這里是一個使用 Redis 的示例:
import redis.clients.jedis.Jedis;public class DistributedIdGenerator {private final SnowflakeIdGenerator snowflakeIdGenerator;private final Jedis jedis;public DistributedIdGenerator(long dataCenterId, long machineId, String redisHost, int redisPort) {this.snowflakeIdGenerator = new SnowflakeIdGenerator(dataCenterId, machineId);this.jedis = new Jedis(redisHost, redisPort);}public long nextId() {long id = snowflakeIdGenerator.nextId();String key = "snowflake:" + id;while (jedis.exists(key)) {id = snowflakeIdGenerator.nextId();key = "snowflake:" + id;}jedis.setex(key, 3600, "1"); // 設置過期時間,避免長期存儲return id;}
}
解釋
-
基本雪花算法:
EPOCH
:自定義的紀元時間。DATA_CENTER_ID_BITS
、MACHINE_ID_BITS
和SEQUENCE_BITS
:數(shù)據(jù)中心 ID、機器 ID 和序列號的位數(shù)。nextId
方法:生成唯一 ID,并處理時鐘回撥問題。
-
分布式唯一 ID:
- 使用 Redis 確保 ID 唯一性:在生成 ID 后,將其存儲在 Redis 中,檢查是否重復。
jedis.setex(key, 3600, "1")
:使用帶過期時間的鍵來避免長期存儲。
-
時鐘回撥處理:
- 當檢測到時鐘回撥時,拋出異?;虻却龝r間前進。
- 使用
tilNextMillis
方法等待直到時間前進。
小結(jié)
這種方案結(jié)合了雪花算法的高性能和 Redis 的分布式存儲能力,解決了時鐘回撥問題,并確保在分布式環(huán)境下生成唯一 ID。通過這些措施,可以實現(xiàn)一個穩(wěn)定、高效的分布式唯一 ID 生成系統(tǒng)。
其他方法
- 復合主鍵:結(jié)合多個字段作為主鍵,適用于表中沒有自然唯一標識符的場景。但增加了查詢和維護的復雜性。
- 業(yè)務相關(guān)ID:如訂單號,易于理解且與業(yè)務緊密相關(guān),但可能需要額外的邏輯來保證唯一性,且擴展性較差。
總結(jié)
選擇哪種主鍵生成策略取決于具體的應用場景:
- 對于單體應用或簡單的分布式系統(tǒng),自增ID可能是最簡單高效的選擇。
- 在分布式系統(tǒng)中,尤其是跨多個數(shù)據(jù)中心時,雪花算法因其高性能和全局唯一性成為優(yōu)選。
- 當全局唯一性是首要考慮因素,且對存儲空間不太敏感時,GUID是合適的選擇。
- 具體場景下,也可以根據(jù)業(yè)務需求考慮復合主鍵或業(yè)務相關(guān)ID的方案。