使用分布式系統(tǒng)架構(gòu)面臨全局ID的生成策略的抉擇,本文描述了筆者所構(gòu)想的一個(gè)優(yōu)化方案,歡迎拍磚!
多種ID生成方式
1. UUID
算法的核心思想是結(jié)合機(jī)器的網(wǎng)卡、當(dāng)?shù)貢r(shí)間、一個(gè)隨記數(shù)來(lái)生成UUID。
- 優(yōu)點(diǎn):本地生成,性能好,沒(méi)有高可用風(fēng)險(xiǎn);
- 缺點(diǎn):長(zhǎng)度過(guò)長(zhǎng),且無(wú)序
2. 數(shù)據(jù)庫(kù)sequence
使用數(shù)據(jù)庫(kù)的id自增策略,如MySQL的auto_increment。并且可以使用兩臺(tái)數(shù)據(jù)庫(kù)分別設(shè)置不同步長(zhǎng),生成不重復(fù)ID的策略來(lái)實(shí)現(xiàn)高可用。
- 優(yōu)點(diǎn):數(shù)據(jù)庫(kù)生成的ID絕對(duì)有序,高可用實(shí)現(xiàn)方式簡(jiǎn)單。
- 缺點(diǎn):需要獨(dú)立部署數(shù)據(jù)庫(kù)實(shí)例,成本高,有性能瓶頸
3.雪花算法
twitter生成全局ID生成器的算法策略。
簡(jiǎn)單來(lái)說(shuō):就是把64的Long型數(shù)據(jù)由以下幾個(gè)部分組成:
符號(hào)位(1位)-時(shí)間戳(41位)-數(shù)據(jù)中心標(biāo)識(shí)(5位)-ID生成器實(shí)例標(biāo)識(shí)(5位)-序列號(hào)(12位)
通過(guò)部署多個(gè)ID生成器,位各個(gè)業(yè)務(wù)系統(tǒng)生成全局唯一的Long型ID。
- 優(yōu)點(diǎn):生成Long型易操作,有序
- 缺點(diǎn):需要獨(dú)立部署id生成器,增加維護(hù)成本
4.MongoDB ObjectId
生成策略類似雪花算法。
時(shí)間戳+機(jī)器ID+進(jìn)程ID+序列號(hào)=>ObjectId對(duì)象
- 優(yōu)點(diǎn):本地生成,有序,成本低
- 缺點(diǎn):使用機(jī)器ID和進(jìn)程ID,64位Long無(wú)法存儲(chǔ),只能生成特殊ObjectId對(duì)象。
總結(jié)對(duì)比
| 方式 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|
| UUID | 本地生成,無(wú)中心,無(wú)性能瓶頸 | 無(wú)序,過(guò)長(zhǎng) |
| MongoDB ObjectId | 本地生成,含時(shí)間戳,有序 | 過(guò)長(zhǎng) |
| 數(shù)據(jù)庫(kù)sequence | 有序 | 中心生成,獨(dú)立部署數(shù)據(jù)庫(kù) |
| 雪花算法 | 有序,Long型 | 中心生成,獨(dú)立部署ID生成器 |
大致的總結(jié)優(yōu)略點(diǎn)如下:
| 方式 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|
| UUID | 本地生成,無(wú)中心,無(wú)性能瓶頸 | 無(wú)序,過(guò)長(zhǎng) |
| MongoDB ObjectId | 本地生成,含時(shí)間戳,有序 | 過(guò)長(zhǎng) |
| 數(shù)據(jù)庫(kù)sequence | 有序 | 中心生成,獨(dú)立部署數(shù)據(jù)庫(kù) |
| 雪花算法 | 有序,Long型 | 中心生成,獨(dú)立部署ID生成器 |
想要的
看了以上,我想要的ID生成策略是怎樣的呢?
64位易操作存儲(chǔ),按時(shí)間有序,無(wú)中心本地生成。
好吧,其實(shí)本文也沒(méi)有完全實(shí)現(xiàn)以上需求,如果哪位小伙伴有更好方案歡迎回復(fù)分享?。?!
本文只是基于對(duì)以上幾種方案的認(rèn)識(shí),稍加改進(jìn),盡可能的滿足需求!
來(lái)吧
我的想法:
使用Long型,不可避免參考雪花算法的實(shí)現(xiàn),但是要實(shí)現(xiàn)本地化生成,要參考ObjectId的生成策略,使用類似機(jī)器ID,進(jìn)程ID來(lái)保證唯一性。
如何解決使用機(jī)器ID,進(jìn)程ID時(shí)導(dǎo)致ID過(guò)長(zhǎng)的問(wèn)題?
解決方式:放棄使用機(jī)器ID,進(jìn)程ID,使用serverId標(biāo)識(shí)服務(wù),使用instanceId標(biāo)識(shí)服務(wù)進(jìn)程,但是。。。沒(méi)辦法,需要一個(gè)中心來(lái)進(jìn)行注冊(cè),保證唯一性,本例中使用Redis(不限于redis,database,memcached都可)。
- 相對(duì)于使用獨(dú)立部署的ID生成器,我想Redis之類的緩存集群是各個(gè)分布式系統(tǒng)架構(gòu)中都會(huì)存在的,這樣可以顯著降低架構(gòu)復(fù)雜度,降低成本。
- 對(duì)redis的依賴較低,可以說(shuō)只需要啟動(dòng)的時(shí)候訪問(wèn)redis即可,后續(xù)本地生成ID。
- 另外serverId是固定不變的,是可以預(yù)先分配好的,比如會(huì)員中心微服務(wù)的serverId分配為10,這是固定不變的。
大致實(shí)現(xiàn)
類似雪花算法的參數(shù)
public class IdGenerator {
// 時(shí)間基線 2016/1/1
private final long timeBaseLine = 1454315864414L;
// 服務(wù)編號(hào)
private volatile long serverId = -1;
//服務(wù)實(shí)例編號(hào)
private volatile long instanceId = -1;
private static final long serverIdBits = 7;
private static final long instanceIdBits = 10;
private static final long sequenceBits = 5;
...
}
- serverIdBits = 7,最多支持128個(gè)服務(wù)。
- instanceIdBits = 10, 每個(gè)服務(wù)支持1024個(gè)實(shí)例。
- sequenceBits = 5,每毫秒可生成的id數(shù)量32個(gè)(由于本地生成,且時(shí)間戳精確到毫秒,同一毫秒內(nèi)的ID沖突不會(huì)像中心那么嚴(yán)重。)
簡(jiǎn)單流程

主要方法:
/**
* 應(yīng)用啟動(dòng)完成后調(diào)用init
*
* @param serverId
*/
public synchronized void init(long serverId) {
this.serverId = serverId;
if (!inited) {
inited = true;
Jedis jedis = new Jedis("localhost", 6379);
ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(1);
RegisterIdCreatorInstanceTask registerIdCreatorInstanceTask = new RegisterIdCreatorInstanceTask(jedis);
// 定義定時(shí)任務(wù),定期調(diào)用redis注冊(cè),續(xù)約instanceId
scheduledService.scheduleWithFixedDelay(registerIdCreatorInstanceTask, 0, RegisterIdCreatorInstanceTask.INTERVAL_SECONDS, TimeUnit.SECONDS);
} else {
System.out.println("已經(jīng)初始化!");
}
}
/**
* 注冊(cè)id生成器實(shí)例
*/
private class RegisterIdCreatorInstanceTask implements Runnable {
private Logger logger = Logger.getLogger(RegisterIdCreatorInstanceTask.class.getCanonicalName());
public static final int INTERVAL_SECONDS = 30;
private Jedis jedis;
private RegisterIdCreatorInstanceTask(Jedis jedis) {
this.jedis = jedis;
}
public void run() {
try {
long srvId = idGenerator.getServerId();
long currentInstanceId = idGenerator.getInstanceId();
String prefixKey = ID_CREATOR_KEY + KEY_SEP + srvId + KEY_SEP;
if (currentInstanceId < 0) {
//注冊(cè)
registerInstanceIdWithIpv4();
} else {
//續(xù)約
String result = jedis.set(prefixKey + currentInstanceId, srvId + KEY_SEP + currentInstanceId, "XX", "EX", INTERVAL_SECONDS * 3);
if (!"OK".equals(result)) {
logger.warning("服務(wù)[" + srvId + "]ID生成器:" + currentInstanceId + "續(xù)約失敗,等待重新注冊(cè)");
registerInstanceIdWithIpv4();
} else {
logger.info("服務(wù)[" + srvId + "]ID生成器:" + currentInstanceId + "續(xù)約成功");
}
}
} catch (JedisException e) {
logger.severe("Redis 出現(xiàn)異常!");
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (idGenerator.getInstanceId() < 0) {
idGenerator.setInited(false);
}
if (jedis != null) {
jedis.close();
}
}
}
private int registerInstanceIdWithIpv4() {
long ip4Value = getIp4LongValue();
// Redis key 格式:key->val , ID_CREATOR:serverId:instanceId -> serverId:instanceId
String prefixKey = ID_CREATOR_KEY + KEY_SEP + serverId + KEY_SEP;
// 需要使用java8
int regInstanceId = registerInstanceId((int) (ip4Value % (maxInstanceId + 1)), (int) maxInstanceId, (v) -> {
String res = jedis.set(prefixKey + v, serverId + KEY_SEP + v, "NX", "EX", INTERVAL_SECONDS * 3);
return "OK".equals(res) ? v : -1;
});
idGenerator.setInstanceId(regInstanceId);
idGenerator.setInited(true);
logger.info("服務(wù)[" + serverId + "]注冊(cè)了一個(gè)ID生成器:" + regInstanceId);
return regInstanceId;
}
/**
* 注冊(cè)instance,成功就返回
*
* @param basePoint
* @param max
* @param action
* @return
*/
public int registerInstanceId(int basePoint, int max, Function<Integer, Integer> action) {
int result;
for (int i = basePoint; i <= max; i++) {
result = action.apply(i);
if (result > -1) {
return result;
}
}
for (int i = 0; i < basePoint; i++) {
result = action.apply(i);
if (result > -1) {
return result;
}
}
return 0;
}
/**
* IPV4地址轉(zhuǎn)Long
*
* @return
*/
private long getIp4LongValue() {
try {
InetAddress inetAddress = Inet4Address.getLocalHost();
byte[] ip = inetAddress.getAddress();
return Math.abs(((0L | ip[0]) << 24)
| ((0L | ip[1]) << 16)
| ((0L | ip[2]) << 8)
| (0L | ip[3]));
} catch (Exception ex) {
ex.printStackTrace();
return 0;
}
}
}
/**
* 獲取ID
*
* @return
*/
public long getId() {
long id = nextId();
return id;
}
private synchronized long nextId() {
if (serverId < 0 || instanceId < 0) {
throw new IllegalArgumentException("目前不能生成唯一性ID,serverId:[" + serverId + "],instanceId:[" + instanceId + "]!");
}
long timestamp = currentTime();
if (timestamp < lastTimestamp) {
throw new IllegalStateException("Err clock");
}
sequence = (sequence + 1) & maxSequence;
if (lastTimestamp == timestamp) {
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
}
lastTimestamp = timestamp;
long id = ((timestamp - timeBaseLine) << timeBitsShift)
| (serverId << serverIdBitsShift)
| (instanceId << instanceIdBitsShift)
| sequence;
return id;
}
如有問(wèn)題歡迎指正??!
如有其他方案,歡迎回復(fù)告知,謝謝?。?!