【JAVA服務端架構】分散式環境下Hilo演算法生成自增編號
常見的編號生成策略有以下幾種方式:
1.數字型自增長,但有時候我們需要編號有一定的長度,並不是像0,1,2,3這種,還有可能會對編號加上一定的前戳
2.使用UUID,但是UUID是無序的,毫無意義的字元,像生成訂單號這種就不太適合
3.使用時間戳,但是在高併發的情況下,還是無法能夠保證唯一性,哪怕加上隨機數,也有一定機率重複
4.使用隨機數和自定義字元,但是很多語言的隨機數都是偽隨機數,例如java的Random,在高併發的情況下游客可能能取出相同的數字
所以,我們需要建立一個系統管理主鍵表,用來管理主鍵,對每次取出來的資料進行+1操作,但是這樣的話有一個非常大的效能問題,每次生成編號的時候,需要查詢這個系統管理主鍵表,對效能有一定的損耗。所以我們使用Hilo高低位演算法來解決這個問題。
生成自增編號保證兩點:
1.首先要保證在分散式環境下編號的唯一性
2.其次保證編號可序性,能夠通過對資料進行排序
做法如下:
1.唯一性
設:最小低位值10 最大低位值99
(為保證分散式特殊情況,最小低位和最大低位位數要相同)
高位值 編號
1 110
1 111
1 112
1 113
…
1 199
2 210
2 211
2 212
…
2 299
…
11 1110
編號生成方式為:高位 + 低位(注意:+是拼接,不是運算子,保證分散式情況下的唯一性)
2.有序性
資料查詢時,可按照高位和編號同時進行升序,或降序排序
對每次從系統管理主鍵表取出資料時,我們需要同時對資料進行+1操作
具體實現如下:
//unique _key public HiloNumber generate() throws Exception { synchronized (HiloOptimizer.class){ if(!isInitialize){ throw new Exception("Hilo Service not initialized."); } //低位進位 low ++ ; if (low > max) { // 當低位超過最大高位 //獲取下一次高位的值 high = queryNextHigh(); //低位重置 low = min; } //接下來拼接ID,此處使用演算法實現,不採用轉字串的形式,提高效率 //取得低位數字長度 final long lowLength = getNumberLength(low); //高位和低位進行拼接 final long id=high * (long)Math.pow(10, lowLength) + low; //封裝返回值物件 mHiloNumber.setId(id); mHiloNumber.setHigh(high); return mHiloNumber; } }
詳細程式碼和示例我會在下面給出,下面讓我們來測試一下併發,看看真實環境是怎樣的
首先我們建立TestDB實體類物件,模擬系統管理主鍵表,然後我們建立TestClient客戶端物件,模擬客戶端獲取ID的過程
測試程式碼如下:
public class HiloTest {
public TestDB db=new TestDB();
public void run(){
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
for (int i = 0; i < 2; i++) {
//啟動時,查詢資料庫
TestClient clinet=new TestClient(this,"客戶端"+i);
fixedThreadPool.execute(clinet);
}
}
}
在TestClient中,我們首先初始化HiloOptimizer物件,設定最小低位10,最大低位99,然後獲取150次,並列印最終結果,在實際情況中我們需要把拿到的編號和最高位儲存到我們需要新增編號的表中,儲存最高位是方便我們進行排序使用。
@Override
public void run() {
mHilo = new HiloOptimizer();
mHilo.init(10,99,this);
for (int i = 0; i < 150; i++) {
HiloNumber hiloNumber;
try {
hiloNumber =mHilo.generate();
final long id = hiloNumber.getId();
final long high= hiloNumber.getHigh();
System.out.println("客戶端:"+clientName+" 高位值:"+high+" 編號:"+id);
} catch (Exception e) {
e.printStackTrace();
}
}
}
然後,我們要實現HiloOptimizer的HiloTask介面,在需要進位的時候對系統管理主鍵表中儲存的最高位進行+1並返回
@Override
public synchronized long queryNextHigh() {
synchronized (TestDB.class){
//從資料庫中查詢當前資料的高位值
long high = mTest.db.high;;
//高位進位
high = high + 1;
//更新資料庫
mTest.db.high=(high);
System.out.println("客戶端:"+clientName+" 查詢更新:"+high);
return high;
}
}
OK,執行結果如下:
以上結果並沒有採用併發來測試,接下來我們在模擬一下併發情況
修改執行緒池同時執行的執行緒數,一般情況是依據CPU的核心數來定的,所以我們這裡暫且設定為4
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
然後執行,執行結果如下:
OK,接下來我們模擬一下高併發生成大量的編號,然後把資料匯入資料庫中,進行排序和查詢
首先使用Mysql隨意建立一張表,欄位就存id,high
修改列印資料格式:
System.out.println(id+","+high);
然後修改測試客戶端數量,改為12
執行,並把結果儲存在txt文字文件中
然後將文字文件資料匯入到mysql資料庫中,如果匯入成功,則證明我們生成的編號沒有重複,因為id欄位是主鍵,主鍵不允許重複,如果重複,則會報錯
開始匯入,此處我使用的視覺化的Mysql工具Navicat for MySQL:
選擇文字文件,下一步,然後選擇分欄符為逗號
設定匯入資料對應的欄位,其中410為編號,4為高位值
選擇如下:
OK,匯入成功,一共1800行資料,沒有異常,所以我們生成的編號是沒有重複的
開啟表,看到效果如下,注意,此處預設是按照主鍵升序排序的