一. 背景介紹
在MySQL中,幾乎所有的數據表都會有一個主鍵,主鍵是不允許重復的,所以表中的每一條數據的id都不會相同。
主鍵id可以是數字,也可以是字符串,一般情況下都會選擇數字做主鍵id,數字類型,又可以分為int、long、float、double這幾大類(可以細分),創建數據表的時候,會根據預期的數據量來選擇數據類型。
對於數字類型這種主鍵,在插入新紀錄的時候,有兩種選擇:1、id自增,插入數據時,忽略id字段(或者設為null,會自動使用自增id);2、插入數據時,手動指定id的值。兩種方式都有各自的優缺點。
二. 主鍵id自增的優缺點
優缺點可以參考網上的博文:https://www.jianshu.com/p/f5d87ceac754
拋開上面的博文中所提到的缺點不談(因為迄今為止我也沒遇到過這樣的問題),我平時遇到更多的問題,是下面這樣的:
1、有一個數據,會多個系統共享。
2、當數據產生后的時候,所有相關的系統,都能根據一個標識(id)來找出這條數據記錄。
你可能會想,數據產生后,我先入庫,因為數據入庫后就有一個主鍵id(自增),然后我將該包含主鍵id的數據查出來,再發給其他系統,這樣所有系統都能知道這條數據的id了。
但是,這里存在一個問題,入庫后,你怎么找出之前入庫的是哪條數據?很多時候,標志數據唯一性的就只有主鍵,這個時候怎么辦?特別是插入頻率特別高的時候,如何准確找到那一條數據?幾乎是不可能的!除非,在插入數據的時候,就指定了id。
三. 手動指定主鍵id的優缺點
手動指定主鍵id的優點(不用id自增),可以解決上面那個問題;
缺點很明顯,就是容易出現主鍵id沖突,當插入頻率達到一定程度時,就會出現大量這種問題,一旦出現這種問題,入庫就會失敗,造成數據丟失(應用層可以做異常處理來避免數據丟失)。
其實,我們只需要解決主鍵id重復的問題就ok了,對吧。
那么怎么生成主不重復的主鍵id呢?特別是當前的分布式架構盛行,大量的數據從不同的節點產生,如何保證每一條數據都能分配到一個唯一的id,這是個問題。
目前已經有很多種算法,比如UUID、SnowFlake算法,還有一些中間件,redis和zk也有自己的唯一id生成算法。
可以參考:
四. UUID算法
關於uuid的算法介紹,可以參考百度百科:https://baike.baidu.com/item/UUID
在Java中UUID工具類(java.util.UUID),可以生成128bit(32字節)的十六進制字符串,由4個“-”將十六進制字符串分隔為5段,規則為8-4-4-4-12,一共是36位,每一段都有特殊的含義,比如:4ab1416b-cafc-4932-b052-cd963aff22eb。
下面是使用示例:
import java.util.UUID;
public class UuidDemo {
public static void main(String[] args) {
final UUID uuid = UUID.randomUUID();
System.out.println(uuid);
// 4ab1416b-cafc-4932-b052-cd963aff22eb
// 一共128bit(32字節),加上4個-分隔符,一共36位,規則是8-4-4-4-12
}
}
五. snowflake算法
snowflake是twitter開源的一種算法,可以生成全局唯一的id。
生成的id是一個64bit的數字,這64bit可以分為4部分,如下圖所示,每一部分都有特殊的含義:

snowflake算法的實現代碼如下:
package cn.ganlixin.ssm.util.common;
public class SnowFlakeIdGenerator {
//初始時間截 (2017-01-01)
private static final long INITIAL_TIME_STAMP = 1483200000000L;
//機器id所占的位數
private static final long WORKER_ID_BITS = 5L;
//數據標識id所占的位數
private static final long DATACENTER_ID_BITS = 5L;
//支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
//支持的最大數據標識id,結果是31
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
//序列在id中占的位數
private final long SEQUENCE_BITS = 12L;
//機器ID的偏移量(12)
private final long WORKERID_OFFSET = SEQUENCE_BITS;
//數據中心ID的偏移量(12+5)
private final long DATACENTERID_OFFSET = SEQUENCE_BITS + WORKER_ID_BITS;
//時間截的偏移量(5+5+12)
private final long TIMESTAMP_OFFSET = DATACENTER_ID_BITS + WORKER_ID_BITS + SEQUENCE_BITS;
//生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095)
private final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
//數據中心ID(0~31)
private long datacenterId;
//工作節點ID(0~31)
private long workerId;
//毫秒內序列(0~4095)
private long sequence = 0L;
//上次生成ID的時間截
private long lastTimestamp = -1L;
/**
* 構造函數
*
* @param datacenterId 數據中心ID (0~31)
* @param workerId 工作ID (0~31)
*/
public SnowFlakeIdGenerator(long datacenterId, long workerId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("WorkerID 不能大於 %d 或小於 0", MAX_WORKER_ID));
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException(String.format("DataCenterID 不能大於 %d 或小於 0", MAX_DATACENTER_ID));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 獲得下一個ID (用同步鎖保證線程安全)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
// 毫秒時間戳(13位)
long timestamp = System.currentTimeMillis();
//如果當前時間小於上一次ID生成的時間戳,說明系統時鍾回退過這個時候應當拋出異常
if (timestamp < lastTimestamp) {
throw new RuntimeException("當前時間小於上一次記錄的時間戳!");
}
//如果是同一時間生成的,則進行毫秒內序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
//sequence等於0說明毫秒內序列已經增長到最大值
if (sequence == 0) {
//阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp);
}
} else {
//時間戳改變,毫秒內序列重置
sequence = 0L;
}
//上次生成ID的時間截
lastTimestamp = timestamp;
//移位並通過或運算拼到一起組成64位的ID
return ((timestamp - INITIAL_TIME_STAMP) << TIMESTAMP_OFFSET)
| (datacenterId << DATACENTERID_OFFSET)
| (workerId << WORKERID_OFFSET)
| sequence;
}
/**
* 阻塞到下一個毫秒,直到獲得新的時間戳
*
* @param lastTimestamp 上次生成ID的時間截
* @return 當前時間戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
測試使用:
package cn.ganlixin.ssm.util;
import org.junit.Test;
public class SnowFlakeGeneratorTest {
@Test
public void testGen() {
SnowFlakeIdGenerator idGenerator = new SnowFlakeIdGenerator(1, 1);
System.out.println(idGenerator.nextId()); // 429048574572105728
}
}
