庫存服務優化思路

庫存服務優化思路

很多業務系統構架中都有庫存服務

  • 庫存可以以數量為單元,假如有5臺手機,那麼庫存就是 5 ,賣掉一臺,庫存就減 1 變成 4 。這種以數量為單位進行的庫存計算相對比較簡單,在架構設計中無論你優化DB中的SQL,還是存儲結構,或者引入Redis做緩存都比較好設計和實現。
  • 有些庫存不是以數量為單位的,比如酒店房間,這是可以重複利用的,是以天為單位的,行話叫“間夜”,理論上如果某間房間一年365天都可用,那麼一年就有365個庫存,一天就一個。A客人1月1號佔了,B客人1月1號就不能佔。當然,還有鐘點房,時間粒度更細,這個先不討論,後面會涉及。
  • 還有些業務的庫存是以更細粒度為單位的,比如小時。租車業務就是以小時為單位進行庫存佔用的,假設我們只有一輛車,A客戶8:00-10:00租用,B客戶可以 11:00-15:00租用。

以下的庫存優化思路是針對租車這種細粒度的佔用單位的

筆者幾年前參與過租車業務中庫存系統的改造升級,當時的庫存服務主要的問題有兩個

  • 沒有形成獨立的服務,與其他服務耦合在一個“全家桶”式的系統中
  • 與庫存相關的業務提供的RPC服務響應較慢,而又由於庫存是核心服務,所以庫存一慢,各相關子系統都會拖慢

對於第一個問題,相對比較好解決,我們將庫存服務獨立出來,包括數據庫。問題在於第二個,其實慢的原因也很明顯,就是對於庫存的計算完全依賴數據庫,利用SQL與java代碼進行計算,當然重頭戲在DB,當時採用的數據庫是SQL Server,幸虧是SQL Server,如果是My SQL的話可能性能撐不到我們對它進行改造的時候。隨著數據量的增加,整個庫存服務在完全依賴DB的情況下,服務的查詢速度較慢,是秒的量級。

當時的改造先進行了一輪謹慎的嘗試,即對業務進行整體重新梳理、對現存SQL,java代碼進行全面優化。優化的結果並不理想,雖然這個過程讓業務更清晰,也發現了些遺留問題和BUG,但是並沒有解決慢的根本問題。

在第一輪的基礎上想過用緩存進行處理的方案,但由於單位粒度太細,緩存的設計方案複雜又不好落地,並沒有採用。在QPS和TPS趨於平穩的情況下,庫存系統的問題並沒有被逐漸放大,反而似乎沒有到大家忍受不了的地步。於是就不了了之了。

而實際上,這件事真的就結束了嗎?我想不一定

由於當時公司的系統越來越多,並且舊系統也會升級改造,隨著一些業務的新增或升級,如果有突發的流量,比如做個營銷活動,系統能不能撐得住?即使撐得住,用戶體驗會不會好?我不知道,但我知道如果一個核心服務慢了,整體的體驗都不會好。

其實對於我個人來講,那次的改造不徹底也是我的一個心結,首先當時沒有明確的標準和目標,沒有特別清晰的量化下來,比如庫存服務優化到300-500ms。其實當時我很清楚它慢,就是沒有很好的解決了那個問題,心有不甘。

於是今天想起來,提出一個方案,看能不能解決它!

整個庫存服務的重點就是如何準確的計算出來它的值,如果算的不對,可能導致有車租不出去(算少了),或沒車租出去了(算多了)。而計算這個值的公式特別簡單:

可預定車輛數 = 現有運營車輛數 - 被佔用車輛數

1 我們將現有運營車輛數以 門店_車型_amount 為 key ,車輛數為 value 存在Redis中。 當現有運營車輛數變化時,在修改DB的同時同步Redis,當然無論是DB還是Redis都是要做高可用架構設計的,以防止單點故障的發生。

2 被佔用車輛原先是以數據庫表的方式存儲的記錄,之前由於時間粒度太細,所以用SQL計算,所以慢,這裡我們採用貪心算法的思路,具體是這樣的:

首先按門店+車型 一對一的將佔車記錄取出來。如A門店C1車型,A門店C2車型。將每組每條佔車記錄的開始和結束時間轉為Unix時間戳,最小單位是秒,如:1546315932。

再將開始和結束時間組成一個閉區間如[1546315932,1546316000],再把每組的這些閉區間組成一個二維數組。int[][]intervals。

例如:

[[1546315932,1546316000],[1546315942,1546317000],[1546315932,1546315943],[1546315901,1546315907]]

最後每一個門店+車型都對應一個佔車二維數組。

將這些數組以 門店_車型_occupy 為 key ,二維數組為 value 序列化到redis中

當要計算某門店某車型的庫存時,先將上面的以門店_車型_occupy為key的值取出,然後利用貪心算法計算出時間段重合的佔用數量,再用門店_車型_amount為key的總量減去佔用數量就得到了可用庫存數。如果時間段不重合就更新Redis二維數組添加新的元素。Redis在操作期間是需要LOCK的。

貪心算法

貪心算法,特別適合解決 Interval Scheduling(區間調度問題),比如算出多個區間中最多有幾個互不相交的區間;或給定一個區間的集合,找到需要移除區間的最小數量,使剩餘區間互不重疊。如果我們請求庫存的參數中的時間與現有佔車記錄的有重疊,那麼返回的需要移除區間的最小數量就是 >=1 的。

代碼實現如下:


<code>public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length == 0) {
return 0;
}
Arrays.sort(intervals,new Comparator(){
@Override
public int compare(int[] o1,int[] o2){
return o1[1]-o2[1];
}
});
int cnt = 1;
int end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] < end) {
continue;
}
end = intervals[i][1];

cnt++;
}
return intervals.length - cnt;
}
/<code>
庫存服務優化思路

由於貪心算法是線性時間複雜度,又是在內存中計算,所以效率會比數據庫高很多。

  • 這裡你可能會關心Reids存儲的數據量,由於車輛的開放預定週期不會太久,比如半年或一年,那麼分到每個門店和車型其實預定的人並不那麼多,即使在未來的預定週期內所有車都被預定了數據量也不很大。
  • 對於過期的數據,比如一個月以前或一週以前的不再參與庫存計算的Redis數據,用定時任務清掉就可以了,或者也可以再行持久化備份一份。
  • 這裡講的是單車型的,如果要算多車型的,就起多線程並行計算。

以上就是利用了緩存和貪心算法進行的緩存優化,在此基礎上還應該注意:不要對原服務(依賴數據庫版本)進行刪除,可做為系統服務降級時使用。Redis也要做好高可用和數據庫降級處理,如果Redis掛了,還可以用DB頂一下,如果數據沒有了,就將整個服務降級到老版本。

需要注意的是,上面的方案我並沒有在生產環境實施過,只是一種單純的設計,如有不嚴謹和不對的地方,歡迎指正。如你需要解決相似的問題,請謹慎參考!

庫存服務優化思路

End


分享到:


相關文章: