目錄java
下面的案例來自筆者的實際工做經歷,涉及到的系統是筆者負責開發和維護的,一個國外的電商平臺。redis
若是你對電商系統有所瞭解,將有助於你理解下面提到的業務。網絡
若是你沒有相關的知識背景,也沒有關係,我會盡量簡化地將業務講給你,而且只要求你理解關鍵概念便可。多線程
事情的原由是平臺的某位高級主管的一封郵件,其中提到商品全量庫存的實時性過低,須要各個部門的人合力解決。架構
先介紹一下電商平臺的一些基本概念。併發
庫存就是倉庫中某個SKU(最小庫存單元)在倉庫中實際有量。分佈式
好比某型號灰色8核16G內存的筆記本電腦就是一個SKU,在倉庫中這個SKU有100臺,那麼它的庫存量就是100。ide
倉庫天天都會把本身實際的庫存量統計出來,這就是全量庫存,倉庫把庫存量發送給各個銷售終端,這就是全量庫存同步。post
同時,爲了保證庫存的實時性,防止超賣(賣出比實際庫存量更多的商品,倉庫沒法發出貨品,有可能致使客訴)和倉庫有貨但客戶買不到的狀況,倉庫會把庫存的變化量也實時分發到各個終端,這個庫存的變化量就是增量庫存。性能
舉例來講,上面的那個SKU筆記本電腦有一臺送到攝影棚去拍照了,那麼這臺就沒法銷售了,倉庫就會推送一個-1的增量庫存到銷售終端;而若是它收到了消費者的退貨,退貨入庫之後,將會推送一個+1的增量庫存。
電商平臺通常都會有多個店鋪入駐,例如3C這個分類下面,可能有蘋果、華爲、三星、小米等店鋪。
不一樣店鋪的庫存是獨立的。
有時候一個SKU在多家店鋪都有售,iPhone X/太空灰色/256GB 在 XXX蘋果平臺旗艦店 、XXX手機大世界店、 XX蘋果折扣店 就是三個不一樣的庫存記錄。
這就是多店鋪庫存。
做爲分銷商,它的倉庫中放着不一樣平臺、不一樣品牌的商品。例如上面的手機,在深圳、廣州、上海三個地區倉庫都有貨,而且是分別賣給天貓和京東的,那麼它的庫存記錄就有6條,分別是:
No. | 倉庫 | 渠道 |
---|---|---|
1 | 深圳 | 天貓 |
2 | 深圳 | 京東 |
3 | 廣州 | 天貓 |
4 | 廣州 | 京東 |
5 | 上海 | 天貓 |
6 | 上海 | 京東 |
這就是分盤庫存。
在實際操做中,爲了保證數據的準確性,平臺會對庫存的時間進行校驗。
例如,倉庫在凌晨 01:00 清點出某SKU庫存量爲100,則這條庫存記錄的庫存清點時間就是01:00。
倉庫在01:00清點完之後,在02:00收到了一個退貨,那麼就會推送一個+1的增量到平臺。
通常狀況下,全量先發出,平臺應該先收到全量100,再收到增量+1,最後爲101。
但若是因爲某個中間環節出了問題,先收到增量+1,在收到全量100,那麼最終的庫存量將是100。全量庫存會直接覆蓋平臺如今的庫存量。
所以,若是有一個最後更新時間,記錄是02:00收到的增量,那麼當01:00的全量過來的時候,因爲比增量時間要早,將被平臺視爲做廢。
實際的庫存數據流轉過程每每不是 「倉庫——>平臺」 這麼短的鏈路,實際鏈路老是很長的:
不一樣系統的性能不一樣,實現方式不一樣,越長的鏈路時延問題就越嚴重。
想要解決問題,首先要分析問題。做爲平臺技術負責人,我先統計了平臺最近一個月的庫存同步時間,大約是150分鐘,而且每隔幾天會延長几分鐘。
而後我統計了最近一段時間全量庫存的數據變化量,僅僅10天就增長了5w。
分析完問題,我當即召開了團隊的人員討論解決方案,通過你們討論,能夠優化的環節是下面幾個:
當你拼命練跑步避免遲到的時候,也許給你一輛車就解決問題了。
部門服務的資源緊張,配置極低。
目前庫存數據拆分粒度很細(分店鋪分倉庫分門店),加上網絡時延,會形成處理時間延長。
庫存數據由消息中心統一處理,消息中心會處理訂單、商品、價格、會員、庫存等等多種類型的數據,效率不高。
從平臺處理數據的代碼流程着手優化。
對於方案1須要金主批錢,方案2須要多個系統修改,這些很差談;方案3須要改動總體架構,工做量巨大。
對於解決燃眉之急,方案4的可行性最高,改動量和影響範圍最小。
方案4優化全量庫存同步,具體細化爲下面三個方面
下面在實施的時候一一詳細說明。
業務精簡和標準化分爲下面幾個方面:
目前全量和增量庫存同步使用同一個隊列名,經過字段判斷是全量仍是增量。 這樣增長了代碼的複雜度,並且原子性很差。 全量庫存單獨隊列,與增量同步隔離。
修改庫存之後須要記錄詳細變動日誌,日誌的實時性要求不高,將改操做剝離爲單獨的隊列進行處理。
目前同步庫存以前先判斷該商品是否存在,若是存在再判斷該商品在庫存表是否有記錄,若是沒有則新建記錄,有則更新庫存。
因爲隨着數據量的增長,新建的記錄(天天1k到3k之間)所佔的比重愈來愈小,所以將新建的操做也單獨剝離爲一個隊列進行處理。
平臺爲分佈式系統,消息處理須要從註冊中心調用遠程Dubbo服務,首先將數據處理移動到Dubbo服務中完成,避免了頻繁調用Dubbo服務,另外使用多線程處理消息,最大限度利用多核心的優點。
關於線程池的使用,能夠參考這篇文章:使用ThreadPoolExecutor構造線程池
//構造線程池
private static ExecutorService executorService = new ThreadPoolExecutor(
16,
32,
10L,
TimeUnit.MINUTES,
new LinkedBlockingQueue<Runnable>(
2048),new ThreadFactoryBuilder()
.setNameFormat("BatchSyncFullInventory-Pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy());
複製代碼
通過上面的優化,目前處理的時間有了大幅度下降:
通過上面的優化,發現每處理2k條消息,處理時間在1s之內,但出隊時間接近15s。
所以,下面的優化重點是提升隊列的操做性能。
因爲Redis頻繁的操做,會形成RTT(網絡時延)不斷延長,可使用管道來下降RTT。
下面是Spring Data Redis使用管道的方式:
//從隊列中循環取出消息, 使用管道, 減小網絡傳輸時間
List<Object> msgList = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (int i = 0; i < batchSize; i++) {
connection.rPop(getQuenueName().getBytes());
}
return null;
}
});
複製代碼
理論上是這樣的,須要有實際的數據支撐,所以須要經過作實驗來驗證方案的效果。
首先,在測試環境對比了三種不一樣的出隊方式的性能,分別是單線程循環出隊、多線程循環出隊和單線程管道出隊。
測試發現使用管道出隊兩千次,只須要70毫秒左右。
最終,使用了 管道+多線程,庫存消息的處理時間降到了30分鐘左右:
關於管道的使用,能夠參考這篇文章:Redis管道技術
雖然發佈到生產之後,處理時間有了大幅度下降,可是通過監控發現,Redis的CPU使用率一直居高不下。
對於監聽隊列的場景,一個簡單的作法是當發現隊列返回的內容爲空的時候,就讓線程休眠幾秒鐘,等隊列中累積了必定量數據之後再經過管道去取,這樣就既能享受管道帶來的高性能,又避免了CPU使用率太高的風險。
//若是消息的內容爲空, 則休眠[10]秒鐘之後再繼續取數據,防止大批量地讀取redis形成CPU消耗太高
if (CollectionUtils.isEmpty(messageList)) {
Thread.currentThread().sleep(10 * 1000);
continue;
}
複製代碼
做爲一個工程師,要知道本身能力的邊界在哪裏,利用有限的資源讓方案落地。
這裏優化的經歷,是想讓你們對電商相關的業務有所瞭解,另外對處理問題的解決思路有所借鑑。