遇到了幾個需要生産唯一鍵的場景
1:倉儲系統,庫内各類單号需要唯一并帶有一定的業務意義
要求:AAAA20151212000000001 AAAA為艙内業務編碼,中間8位為對應的年月日,後8位為唯一鍵保障。
2:電商訂單系統,訂單号需要唯一
要求:28710080047686 後四位為買家Id(for分庫分表),前面資料為唯一鍵保障位。
3:聊天app系統注冊使用者Id與評論Id唯一鍵需求。
要求:etx32s 使用者Id唯一值
三個需求中,都要求有一個能保障序列号唯一的鍵值。分布式系統中,保障鍵值唯一的方案較多。比如:簡單粗暴的DB自增序列,MD5(基本唯一),以及在場景一中,當時用的tair(阿裡中間件)做的分布式鎖保障自增唯一。本次介紹一個sequence生成中間件産品的設計方案。
整體方案:
ps:
整體方案思路:
應用叢集每次擷取一個id端,本地緩存。每次使用時,本地擷取。當本地id段使用完之後,再進行一次遠端調用,更新。
服務端,DB做主從配置,采用MHA異常主從切換。
容量分析:
叢集10wqps,500台伺服器。單機id段設定1w長度,對伺服器端的請求壓力不到1qps,服務端無壓力,且絕大部分為本地擷取,無rpc消耗。
example:
1:用戶端初始化
//初始化用戶端Util
public static IdGenerator instance(String... password) {
IdGenerator ig = generators.get(key);
if (ig == null) {
synchronized(generators) {
ig = generators.get(key);
if (ig == null) {
ig = new IdRangeIncrementGenerator(tmpPassword, idRangeDispatcher);
generators.putIfAbsent(key, ig);
}
}
}
return ig;
}
//初始化用戶端序列化段與本地Queue
public IdRangeIncrementGenerator(final String schema, final String table, final String password,
IdRangeDispatcher dispatcher) throws NoSuchGeneratorException {
super(dispatcher);
checkArgument(isNotBlank(password), "The password is blank");
this.password = password;
//異步擷取idRange
backThread = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
IdRange idRange = retryNext(password);//調用服務端、走DB擷取Id端
backQueue.put(new IdRangeRound(idRange));
} catch (InterruptedException ie) {
logger.error("interrupted.");
Thread.currentThread().interrupt();
break;
} catch (Throwable e) {
logger.error("backThread retryNext() error", e);
}
}
}
});
backThread.setName("Raptor-IdGenerator-backThread");
backThread.setDaemon(true);
backThread.start();
IdRangeRound idRange = reload(idRangeRound);
String msg = "Can't get idRange by password[" + password + "]";
checkNotNull(idRange, msg);
}
2 用戶端擷取Id段使用
//直接本地方法擷取
@Override
public long next(int num) throws NoMoreIdException {
checkArgument(num > 0, "The num[" + num + "] must be > 0 ");
long[] ids = new long[num];
for (int i = 0; i < num; i++) {
long id;
IdRangeRound ir = idRangeRound;
while ((id = ir.next(1)) == -1) {
//當本地id段不夠使用時,重新遠端擷取更新id段
ir = reload(ir);
}
ids[i] = id;
}
return ids;
}
//阻塞線程更新,重新擷取Id端
private synchronized IdRangeRound reload(IdRangeRound ir) {
if (ir != idRangeRound) {
return idRangeRound;
}
try {
return idRangeRound = backQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
3 服務端DB容災方案:MHA