天天看點

mysql高并發生成id_如何快速開發一個支援高效、高并發的分布式ID生成器

ID生成器是指能産生不重複ID服務的程式,在背景開發過程中,尤其是分布式服務、微服務程式開發過程中,經常會用到,例如,為使用者的每個請求産生一個唯一ID、為每個消息産生一個ID等等,ID生成器也是進行無狀态服務開發的重要需求之一。

ID生成器有其特殊要求:

(1)    産生的ID不能重複,在任何情況下産生的ID都不能重複,例如:在ID生成器程式重新開機之後,ID生成器産生的新ID不能與重新開機之前産生的ID重複;

(2)    ID盡可能短小,由于很多情況下,ID需要被存儲或者傳輸,是以在滿足ID不重複的基礎上ID長度盡可能短小,以降低存儲或傳輸資源的浪費;

(3)    支援高并發低延時請求,ID生成器需要應付大量的請求服務,是以要提供高并發的請求,同時要求請求響應的延時時間盡可能小,一般在10毫秒以内;

(4)    支援多ID,在實際開發過程中,有太多的情形都需要ID服務,例如:每個消息、每個使用者請求、每次事務的處理等等,這些不同的應用場景都需要自己的ID,是以ID生成器需要滿足同時為不各應用場景産生不同ID的服務;

(5)    在特殊場合下還需要滿足ID有序,例如遞增或者遞減;

(6)    高可靠服務,在ID生成器的實際部署過程中,可能會遇到斷電、斷網、程式異常崩潰等等各種奇怪的問題,是以,如何能保證ID生成器提供穩定高效的服務,是分布式服務開發過程中,首先要解決的問題。

下面将介紹如何使用redis的INCR來快速開發一個滿足上述需要的ID生成器服務,INCR指令是Redis針對value為字元串操作,它要求value的最大值為64位,每次操作将redis中的value值加1,并傳回遞增之後的值。其架構如下所示:

mysql高并發生成id_如何快速開發一個支援高效、高并發的分布式ID生成器

使用redis作為ID生成器的資料源,為滿足ID生成器的特殊要求,需特殊部署redis:

(1)    要啟用兩個獨立的redis,這裡的兩個redis需要獨立部署,盡量不要和其他業務複用redis;

(2)    兩個redis一個為主一個為從,主提供ID生産服務,從做持久化,進而保證資料源的高效、安全;

上述部署中,按照如下方式滿足ID生成器的特殊需求:

(1)    用主redis提供産生ID服務,但不進行持久化操作,Redis本身的高性能可滿足ID生成器的高性能和低延時的要求,在實際測試過程中,可産生300多萬/秒的ID,且請求延時為1~2毫秒;

(2)    用從Redis做持久化,保證在master挂掉之後,slave能提供服務,主從全部挂掉之後,ID資料源也被持久化到了硬碟,進而保證重新啟動redis之後,ID會從持久化的位置開始産生,就不會再産生重複的ID;

(3)    redis本身的key-value機制就可以提供多種ID服務,隻需要為每個需求的ID産生不同的key值即可,例如在redis中消息ID的key為“id.msg”,日志ID的key值為“id.log”等等,這樣消息的ID和日志的ID就可以獨自完成遞增操作而互補幹擾。

(4)    redis的INCR和DECR兩個指令,可以産生遞增或者遞減的ID服務;

(5)    redis的ID的類型最大長度為64位,可以從0開始,依次遞增産生,所産生的ID就是最短的;

如果項目緊急,使用上述方式就可以很快搭建一個ID生成器服務,但是上訴ID生成器服務還存在一些缺陷:

(1)    與應用耦合高,沒有對外屏蔽掉内部實作細節,例如redis,在使用時,使用者完全不需要知道ID生成器使用什麼産生的ID;

(2)    擴充性差,在項目規模較大時,ID的應用會非常多,如果用一個redis無法滿足需求時,不方面擴充;

真正的ID生成器服務還需解決上述問題。

前面介紹的是利用redis快速搭建一個ID生成器服務,這種方式搭建的ID生成器服務還存在一些缺陷:

(1)    與應用耦合高,沒有對外屏蔽掉内部實作細節,例如redis,使用者完全不需要知道ID生成器使用什麼産生的ID;

(2)    擴充性差,在項目規模較大時,ID的應用會非常多,如果用一組redis無法滿足需求時,不方面擴充;

下面将對上述的ID生成器進一步改進,改進方式為通過thrift将redis封裝起來,形成一個獨立的ID生成器服務,對外以rpc方式提供ID服務,采用thrift架構可以帶來如下好處:

(1)    高性能,thrift架構提供多種服務運作方式,能提供高性能的rpc服務;

(2)    多語言支援,thrift架構對多語言支援非常好,而ID生成器本身作為一個基礎的、通用性的服務,它需要為各種應用場景都能提供ID服務,是以多語言的支援對其提供服務非常有幫助。

修改之後的架構如下所示:

mysql高并發生成id_如何快速開發一個支援高效、高并發的分布式ID生成器

上述架構中,IdGen是通過thrift架構開發的ID生成器服務,為無狀态服務,可根據需要水準擴充,它内部管理多個redis主從對,每個redis主從對都按照:主Redis提供服務不持久化,從Redis不提供服務但持久化的方式;IdGen内部屏蔽對redis的操作,并完成對redis主從對的管理,它可根據ID的類型,将不同的Id類型放在不同的redis主從對中,這樣在項目規模擴大,ID的類型和請求量增加時,通路壓力将會被分散到各個Redis分組中;

另外IdGen需要能動态管理資料源Redis的分組,即滿足兩個條件:

(1)    動态添加新的ID類型;

(2)    動态添加Redis分組;

要實作上述兩種要求的方法非常多,最簡單的可以采用資料庫來存儲Redis分組資訊和ID類型與Redis分組的對應關系,如下圖所示:

mysql高并發生成id_如何快速開發一個支援高效、高并發的分布式ID生成器

在該架構中,提供如下方式的RPC接口服務:

long getId(String idType)

調用方隻需提供他需要的ID類型,ID生成器即可為之産生一個對應類型的ID。

前面兩個ID生成器隻是簡單的完成功能,如果實際應用到生産環境,則對ID生成器的要求更高,具體包括但不限于以下幾點:(1) 産生全局唯一、且單調遞增的ID;

(2) 任何情況下ID不能重複或者回退;

(3) 具備高效率産生ID的能力;

(4) 具備提供多種ID的能力;

(5) 便于運維管理;

本文檔設計的 ID生成器整個系統需要分為四個部分:web管理端、IdGen服務、redis以及mysql,其中:

(1)web管理端用于運維人員通過網頁形式管理ID生成器,它可以添加/删除ID、遷移ID等;

(2)IdGen服務屬于自研服務,采用Thrift架構,對外提供各種語言的調用接口,對内管理redis和mysql,可水準擴充;

(3)Redis,在本設計中,采用redis作為Id分發器,一個redis可以分發若幹類型的ID,當一個redis無法承載請求量時,可以将id類型遷移到新的Redis上。

(4)Mysql,在本設計中,采用Mysql做Id的持久化存儲和配置管理,隻用一個mysql即可,在實際應用中,還可以采用主從配置提升資料儲存的安全性,但是從Mysql隻做資料儲存,不提供服務,防止主mysql所在主機出現問題時,資料可以正常恢複。 如下圖所示:

mysql高并發生成id_如何快速開發一個支援高效、高并發的分布式ID生成器

擷取id的接口每調用一次,ID生成器就産生一個id,在接口中每次擷取到一個id之後,都需要判斷該配置設定的id值:curID;如下圖所示,如果curID < updateID,這裡update是更新IP段時對應的ID(目前ID段損耗達到所設定比例對應的ID值),則傳回該配置設定的ID;如果updateID < curID < endID,則要産生一個ID更新任務,如果curID >=endID,則直接傳回失敗。

mysql高并發生成id_如何快速開發一個支援高效、高并發的分布式ID生成器

每個IdGen都有一個線程池,用于執行異步任務,在更新ID資訊的任務中,該任務包含以下資訊:ID的名稱,該ID所屬的Redis、該ID在IdGen中緩存的對象,此任務需要計算申請ID段的大小,其方法為根據目前段的使用速度來計算:(目前ID-起始ID)/(目前時間-本段加載時間) * 最大加載間隔時間如果計算的結果大于配置參數的上限,則使用上限值,反之,如果小于配置的下限值,則使用下限值。

此更新ID資訊的任務将完成以下操作:

(1) 從緩存中重新讀取目前id的資訊,并與本地儲存的資訊比較,如果不一緻(結束ID、更新ID和更新時間都大于本地儲存的值),否則說明緩存已經被其他IdGen更新,隻需将本地緩存的資料(本段ID的加載時間、起始ID、結束ID、更新ID)更新為Redis中擷取的資料即可;否則,說明該ID在Redis中的資料還未更新,則繼續執行;

(2)在對應Redis中,對該ID加分布式鎖,如果加鎖失敗,說明有其他的IdGen正在更新該ID的資訊,直接結束目前任務即可;否則,說明目前的IdGen擷取了更新該ID資訊的權利,繼續執行;

(3)計算新申請ID段的大小newIdLen,從資料庫中申請新的ID段,提供參數包括:newIdLen、目前系統時間curSysTime,采用存儲過程完成,具體需完成對資料庫的以下操作: 如果目前ID類型不是有效,則直接傳回-1; 将start_id字段更新為start_id + newIdLen; 将last_range字段更新為newIdLen; 将last_load_time更新為目前系統時間; 傳回新的start_id

(4)IdGen根據start_id計算該ID段的end_id、update_id,并更新Redis中該ID的相關資訊end_id、update_id、目前系統時間更新為新的值,并将startid設定為目前正在配置設定的ID值;

(5)更新本地緩存中該ID的資訊:end_id、update_id、start_id和start_time。

附帶一個開源的ID生成器,該ID生成器按照此文檔的設計思路開發,其源碼位置為:https://github.com/xiaoyaozixyz/idgenerator