天天看點

配運基礎資料緩存瘦身實踐

作者:京東雲開發者

作者:京東物流 張仲良

一、背景:

在現代物流的實際作業流程中,會有大量關系到營運相關資訊的資料産生,如商家,車隊,站點,分揀中心,客戶等等相關的資訊資料,這些資料直接支撐齊了物流的整個業務流轉,具有十分重要的地位,那麼對于這一類資料我們需要提供基本的增删改查存的能力,目前京東物流的基礎資料是由中台配運組來整體負責。

在基礎資料的正常能力當中,資料的存取是最基礎也是最重要的能力,為了整體提高資料的讀取能力,緩存技術在基礎資料的場景中得到了廣泛的使用,下面會重點展示一下配運組近期針對資料緩存做的瘦身實踐。

二、方案:

這次優化我們挑選了商家基礎資料和C背景2個系統進行了緩存資料的優化試點,從結果看取得了非常顯著的成果,節省了大量的硬體資源成本,下面的資料是優化前後的緩存使用情況對比:

商家基礎資料Redis資料量由45G降為8G;

C背景Redis資料量由132G降為7G;

從結果看這個優化的力度太大了,相信大家對如何實作的更加好奇了,那接下來就讓我們一步步來看是如何做到的吧!

首先目前的商家基礎資料使用@Caceh注解元件作為緩存方式,它會将從db中查出的值放入本地緩存及jimdb中,由于該元件早期的版本沒有jimdb的預設過期時間且使用注解時也未顯式聲明,造成早期大量的key沒有過期時間,進而形成了大量的僵屍key。

是以如果我們可以找到這些僵屍key并進行優化,那麼就可以将緩存進行一個整體的瘦身,那首先要怎麼找出這些key呢?

2.1 keys指令

可能很多同學會想到簡單粗暴的keys指令,周遊出所有的key依次判斷是否有過期時間,但Redis是單線程執行,keys指令會以阻塞的方式執行,周遊方式實作的複雜度是O(n),庫中的key越多,阻塞的時間會越長,通常我們的資料量都會在幾十G以上,顯然這種方式是無法接受的。

2.2 scan指令

redis在2.8版本提供了scan指令,相較于keys指令的優勢:

  • scan指令的時間複雜度雖然也是O(N),但它是分次進行的,不會阻塞線程。
  • scan指令提供了類似sql中limit參數,可以控制每次傳回結果的最大條數。

當然也有缺點:

  • 傳回的資料有可能會重複,至于原因可以看文章最後的擴充部分。
  • scan指令隻保證在指令開始執行前所有存在的key都會被周遊,在執行期間新增或删除的資料,是不确定的即可能傳回,也可能不傳回。

2.3基本文法

目前看來這是個不錯的選擇,讓我們來看下指令的基本文法:

SCAN cursor [MATCH pattern] [COUNT count]

  • cursor:遊标
  • pattern:比對的模式
  • count:指定從資料集裡傳回多少元素,預設值為10

2.4 實踐

首先感覺上就是根據遊标進行增量式疊代,讓我們實際操作下:

配運基礎資料緩存瘦身實踐

看來我們隻需要設定好比對的key的字首,循環周遊删除key即可。

可以通過Controller或者調用jsf接口來觸發,使用雲redis-API,demo如下:

配運基礎資料緩存瘦身實踐

好的,大功告成.在管理端執行randomkey指令檢視.發現依然存在大量的無用key,貌似還有不少漏網之魚,這裡又是怎麼回事呢?

下面又到了喜聞樂見的踩坑環節。

2.5 避坑指南

通過增加日發現,傳回的結果集為空,但遊标并未結束!

其實不難發現scan指令跟我們在資料庫中按條件分頁查詢是有别的,mysql是根據條件查詢出資料,scan指令是按字典槽數依次周遊,從結果中再比對出符合條件的資料傳回給用戶端,那麼很有可能在多次的疊代掃描時沒有符合條件的資料。

我們修改代碼使用scanResult.isFinished()方法判斷是否已經疊代完成。

配運基礎資料緩存瘦身實踐

至此程式運作正常,之後通過傳入不同的比對字元,達到清楚緩存的目的。

三、課後擴充

這裡我們探讨重複資料的問題:為什麼周遊出的資料可能會重複?

3.1 重複的資料

首先我們看下scan指令的周遊順序:

配運基礎資料緩存瘦身實踐

Redis中有3個key,我們用scan指令檢視發現周遊順為0->2->1->3,是不是感到奇怪,為什麼不是按0->1->2->3的順序?

我們都知道HashMap中由于存在hash沖突,當負載因子超過某個門檻值時,出于對連結清單性能的考慮會進行Resize操作.Redis也一樣,底層的字典表會有動态變換,這種掃描順序也是為了應對這些複雜的場景。

3.1.1 字典表的幾種狀态及使用順序掃描會出現的問題

  • 字典表沒有擴容

    字段tablesize保持不變,順序掃描沒有問題

  • 字典表已擴容完成
配運基礎資料緩存瘦身實踐

假設字典tablesize從8變為16,之前已經通路過3号桶,現在0~3号桶的資料已經rehash到8~11号桶,若果按順序繼續通路4~15号桶,那麼這些元素就重複周遊了。

  • 字典表已縮容完成
配運基礎資料緩存瘦身實踐

假設字典tablesize從16縮小到8,同樣已經通路過3号桶,這時8~11号桶的元素被rehash到0号桶,若按順序通路,則周遊會停止在7号桶,則這些資料就遺漏掉了。

  • 字典表正在Rehashing

    Rehashing的狀态則會出現以上兩種問題即要麼重複掃描,要麼遺漏資料。

3.1.2 反向二進制疊代器算法思想

我們将Redis掃描的遊标與順序掃描的遊标轉換成二進制作對比:

配運基礎資料緩存瘦身實踐

高位順序通路是按照字典sizemask(掩碼),在有效位上高位加1。

舉個例子,我們看下Scan的掃描方式:

1.字典tablesize為8,遊标從0開始掃描;

2.傳回用戶端的遊标為6後,字典tablesize擴容到之前的2倍,并且完成Rehash;

3.用戶端發送指令scan 6;

配運基礎資料緩存瘦身實踐

這時scan指令會将6号桶中連結清單全部取出傳回用戶端,并且将目前遊标的二進制高位加一計算出下次疊代的起始遊标.通過上圖我們可以發現擴容後8,12,10号槽位的資料是從之前0,4,2号槽位遷移過去的,這些槽位的資料已經周遊過,是以這種周遊順序就避免了重複掃描。

字典擴容的情況類似,但重複資料的出現正是在這種情況下:

還以上圖為例,再看下縮容時Scan的掃描方式:

1.字典tablesize的初始大小為16,遊标從0開始掃描;

2.傳回用戶端的遊标為14後,字典tablesize縮容到之前的1/2,并完成Rehash;

3.用戶端發送指令scan 14;

這時字典表已完成縮容,之前6和14号桶的資料已經Rehash到新表的6号桶中,那14号桶都沒有了,要怎麼處理呢?我們繼續在源碼中找答案:

配運基礎資料緩存瘦身實踐

即在找目标桶時總是用目前hashtaba的sizemask(掩碼)來計算,v=14即二進制000 1110,目前字典表的掩碼從15變成了7即二進制0000 0111,v&m0的值為6,也就是說在新表上還要掃一遍6号桶.但是縮容後舊表6和14号桶的資料都已遷移到了新表的6号桶中,是以這時掃描的結果就出現了重複資料,重複的部分為上次未縮容前已掃描過的6号桶的資料。

結論:

當字典縮容時,高位桶中的資料會合并進低位桶中(6,14)->6,scan指令要保證不遺漏資料,是以要得到縮容前14号桶中的資料,要重新掃描6号桶,是以出現了重複資料.Redis也挺難的,畢竟魚和熊掌不可兼得。

總結

通過本次Redis瘦身實踐,雖然是個很小的工具,但确實帶來的顯著的效果,節約資源降低成本,并且在排查問題中又學習到了指令底層巧妙的設計思想,收貨頗豐,最後歡迎感興趣的小夥伴一起交流進步。

繼續閱讀