參考文章:
- https://blog.csdn.net/hengyunabc/article/details/44244951
- https://www.jianshu.com/p/955909e1bd71
- https://tech.meituan.com/2017/04/21/mt-leaf.html
參考項目:https://github.com/hengyunabc/redis-id-generator值。
evalsha教程:https://www.runoob.com/redis/scripting-evalsha.html
eval教程:https://www.runoob.com/redis/scripting-eval.html
一、分佈式id生成器需要滿足的要求
1.全局唯一
2.儘可能保證id的遞增
因爲在查詢的時候經常會有例如分頁以及排序之類的需求,這個時候如果主鍵的id本身能夠體現出時許效率會更加好。而對於常見的排序還有分頁,我們解決辦法有兩種:
- 在數據表中添加一個時間字段,對其創建一個普通索引。
- id本生就是按照時間大致有序的。
因爲常見的普通索引的訪問效率是比聚集索引要慢的,所以我們儘可能使用第二種解決方案
3.其他的一些要求
- id要儘可能的短,這樣可以減少存儲的空間以及增加查詢的效率。
- 要有足夠數量的id可以使用,不然當數據量非常大時,id耗盡就不行了
- 要考慮不同機器之間的時間不一致問題
- QPS儘量要高,這樣就可以,否則例如像類SNOWFLAKE算法會在64位ID中利用部分位數(如12)表示單位時間內生成的ID序號,這部分序號用完了,這個單位時間就不能再生成序號了
二、常見的id生成器方案:
1.利用mysql數據庫的自增主鍵特性
優點:
- 簡單,代碼方便,性能還行
- 數字的id是遞增的,方便進行分頁和排序
缺點:
- 不同的數據庫語法和實現不同,實現數據遷移或者多數據庫版本的時候可能會出現一些問題
- 我們常見的是一主多從數據庫,這會產生單點故障,以及性能瓶頸
- 數據量大時需要考慮分庫分表
優化方案:
- 使用多個master,對每個master設置的初始id不同,步長不同,例如有四個master,我們可以讓master1生成(1,5,9),master2生成(2,6,10),master3生成(3,7,11),master4生成(4,8,12),這樣可以降低單個數據庫的壓力
2.UUID
常見的一種分佈式id生成器,可以利用數據庫也可以利用代碼。
優點:
- 簡單,方便
- 生成id的性能好,基本上不會有性能問題
- 全球唯一,對於數據庫合併,遷移等問題不會存在衝突
缺點
- 不是有序的
- UUID的字符串長度較長,查詢效率不高,且消耗存儲空間比較大,如果是海量數據庫就需要考慮存儲量的問題了
- 可讀性差
3.redis生成id
redis的大致原理和普通數據庫的生成原理是大致相同的,只不過redis不是使用自增組件,而是使用原子操作 INCR和INCRBY來實現。
優點:
- 不依賴於數據庫,靈活方便,且性能優於數據庫。
數字ID天然排序,對分頁或者需要排序的結果很有幫助。
缺點:
- 如果系統中沒有Redis,還需要引入新的組件,增加系統複雜度。
- 需要編碼和配置的工作量比較大。
4.snowflake算法
- 一個ID由64位生成
- 41bit作爲時間戳,記錄當前時間到標記的起始時間(如到2018.1.1)差,精確到毫秒,那麼服務可用時長爲(1<<41)/(1000* 60 * 60 * 24 * 365) = 69.73年
- 10bit作爲機器ID,也就是可以有1024臺機器
- 12bit作爲序列號,代表單位時間(這裏是毫秒)內允許生成的ID總數,也就是1ms內允許生成4096個ID
優點:
- 不依賴於數據庫,靈活方便,且性能優於數據庫。
- ID按照時間在單機上是遞增的。
缺點:
- 在單機上是遞增的,但是由於涉及到分佈式環境,每臺機器上的時鐘不可能完全同步,也許有時候也會出現不是全局遞增的情況。
5.類SNOWFLAKE算法
SNOWFLAKE給出的主要是一個思想,把ID劃分爲多個段,有不同的含義,可以結合自己的要求進行重新劃分。按照個人理解,時間戳位數少了,機器位數多了,序列號位數多了。借鑑snowflake的思想,結合各公司的業務邏輯和併發量,可以實現自己的分佈式ID生成算法。
舉例,假設某公司ID生成器服務的需求如下:
- 單機高峯併發量小於1W,預計未來5年單機高峯併發量小於10W
- 有2個機房,預計未來5年機房數量小於4個
- 每個機房機器數小於100臺
- 目前有5個業務線有ID生成需求,預計未來業務線數量小於10個
分析過程如下:
- 高位取從2017年1月1日到現在的毫秒數(假設系統ID生成器服務在這個時間之後上線),假設系統至少運行10年,那至少需要10年 * 365天 * 24小時 * 3600秒 * 1000毫秒 = 320 * 10 ^ 9,差不多預留39bit給毫秒數
- 每秒的單機高峯併發量小於10W,即平均每毫秒的單機高峯併發量小於100,差不多預留7bit給每毫秒內序列號
- 5年內機房數小於4個,預留2bit給機房標識
- 每個機房小於100臺機器,預留7bit給每個機房內的服務器標識
- 業務線小於10個,預留4bit給業務線標識
這樣設計的64bit標識,可以保證:
- 每個業務線、每個機房、每個機器生成的ID都是不同的
- 同一個機器,每個毫秒內生成的ID都是不同的
- 同一個機器,同一個毫秒內,以序列號區區分保證生成的ID是不同的
- 將毫秒數放在最高位,保證生成的ID是趨勢遞增的
缺點:
- 由於“沒有一個全局時鐘”,每臺服務器分配的ID是絕對遞增的,但從全局看,生成的ID只是趨勢遞增的(有些服務器的時間早,有些服務器的時間晚)
三、實現一個簡易的redis的id生成器
利用redis的lua腳本執行功能,在每個節點上通過lua腳本生成唯一id,其中使用的是雪花算法。
生成的ID是64位的:
- 使用41 bit來存放時間,精確到毫秒,可以使用41年。
- 使用12 bit來存放邏輯分片ID,最大分片ID是4095
- 使用10 bit來存放自增長ID,意味着每個節點,每毫秒最多可以生成1024個ID
比如GTM時間 Fri Mar 13 10:00:00 CST 2015
,它的距1970年的毫秒數是 1426212000000,假定分片ID是53,自增長序列是4,則生成的ID是:
// 左移22位,指代最前面14bit的存儲信息,再左移10位表示中間存儲分片信息的12bit
5981966696448054276 = 1426212000000 << 22 + 53 << 10 + 4
redis提供了TIME命令,可以取得redis服務器上的秒數和微秒數。因些lua腳本返回的是一個四元組。
second, microSecond, partition, seq
客戶端要自己處理,生成最終ID。
((second * 1000 + microSecond / 1000) << (12 + 10)) + (shardId << 10) + seq;
seq對應的是集羣中的節點值
如集羣裏有3個節點,則節點1返回的seq是:
0, 3, 6, 9, 12 ...
節點2返回的seq是
1, 4, 7, 10, 13 ...
節點3返回的seq是
2, 5, 8, 11, 14 ...
我們可以將lua腳本轉換成sha1值,然後通過EVALSHA指令傳遞這個
下面我們直接看代碼
項目主程序
public class Main {
public static void main(String[] args) {
String tab = "order";
long userId = 123456789;
IdGenerator idGenerator = IdGenerator.builder()
.addHost("127.0.0.1", 6379, "c5809078fa6d652e0b0232d552a9d06d37fe819c")
// .addHost("127.0.0.1", 7379, "accb7a987d4fb0fd85c57dc5a609529f80ec3722")
// .addHost("127.0.0.1", 8379, "f55f781ca4a00a133728488e15a554c070b17255")
.build();
long id = idGenerator.next(tab, userId);
System.out.println("id:" + id);
List<Long> result = IdGenerator.parseId(id);
System.out.println("miliSeconds:" + result.get(0) + ", partition:"
+ result.get(1) + ", seq:" + result.get(2));
}
}
id生成器相關代碼
public class IdGenerator {
static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
/**
* JedisPool, luaSha
*/
List<Pair<JedisPool, String>> jedisPoolList;
int retryTimes;
int index = 0;
private IdGenerator() {
}
private IdGenerator(List<Pair<JedisPool, String>> jedisPoolList,
int retryTimes) {
this.jedisPoolList = jedisPoolList;
this.retryTimes = retryTimes;
}
static public IdGeneratorBuilder builder() {
return new IdGeneratorBuilder();
}
static class IdGeneratorBuilder {
List<Pair<JedisPool, String>> jedisPoolList = new ArrayList();
int retryTimes = 5;
public IdGeneratorBuilder addHost(String host, int port, String luaSha) {
jedisPoolList.add(Pair.of(new JedisPool(host, port), luaSha));
return this;
}
public IdGeneratorBuilder retryTimes(int retryTimes) {
this.retryTimes = retryTimes;
return this;
}
public IdGenerator build() {
return new IdGenerator(jedisPoolList, retryTimes);
}
}
public long next(String tab) {
return next(tab, 0);
}
public long next(String tab, long shardId) {
for (int i = 0; i < retryTimes; ++i) {
Long id = innerNext(tab, shardId);
if (id != null) {
return id;
}
}
throw new RuntimeException("Can not generate id!");
}
Long innerNext(String tab, long shardId) {
index++;
Pair<JedisPool, String> pair = jedisPoolList.get(index
% jedisPoolList.size());
JedisPool jedisPool = pair.getLeft();
String luaSha = pair.getRight();
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
List<Long> result = (List<Long>) jedis.evalsha(luaSha, 2, tab, "" + shardId);
long id = buildId(result.get(0), result.get(1), result.get(2),
result.get(3));
return id;
} catch (JedisConnectionException e) {
if (jedis != null) {
jedisPool.returnBrokenResource(jedis);
}
logger.error("generate id error!", e);
} finally {
if (jedis != null) {
jedisPool.returnResource(jedis);
}
}
return null;
}
public static long buildId(long second, long microSecond, long shardId,
long seq) {
long miliSecond = (second * 1000 + microSecond / 1000);
return (miliSecond << (12 + 10)) + (shardId << 10) + seq;
}
public static List<Long> parseId(long id) {
long miliSecond = id >>> 22;
// 2 ^ 12 = 0xFFF
long shardId = (id & (0xFFF << 10)) >> 10;
long seq = id & 0x3FF;
List<Long> re = new ArrayList<Long>(4);
re.add(miliSecond);
re.add(shardId);
re.add(seq);
return re;
}
}
至此我們的基於redis的id生成器就完成了