使用Redis做預定庫存緩衝功能
最近在自己的工作中,把其中一個PHP項目的緩衝從以前的APC緩衝逐漸切換到Redis中,並且根據Redis所支援的資料結構做了庫存維護功能。緩衝是在業務層做的,準確講應該是在MVC模型中Model的ORM裡面。主要邏輯就是先查緩衝,查不到的話再查資料庫。不過這些不是本文的主要內容,下面我把庫存管理功能的緩衝設計思路分享一下,希望能帶給大家一些收穫,有不足之處或者有更好方案的,也希望各位多多指教。
一、業務背景
為了略去我們公司項目背景,我決定把這次的問題類比成一個考卷上的問題。至於業務細節,大家也無需關注~看題目就可以了:
假設你是某國最牛的收藏家,手裡有各種價值連成的寶物。知道有一天,你覺得做收藏太沒意思了,打算把這些寶物賣掉換點現金。
不過把這些值錢的寶貝放在菜市場上賣實在太low了。在“互連網+”時代,我們當然要玩一些不一樣的賣法:在你名下有一棟300個房間的大樓(編號為001至300),每個房間放著一個密碼鎖保險箱,在下個月(12月1日至12月31日)的每一天,你都會挑選300件最好的“極品寶物”(也稱作A類寶物),分別放入這300個房間的保險箱裡,每天每個房間放什麼寶物已經定好了,所有想買寶物的人必須至少提前一天在網上預定,到時候憑藉預定碼自己開啟保險箱取貨。沒有被預定的寶物將會被你收回,不再售賣。
要做這樣一個網路預定系統,它的前端介面大概是這樣的:
中三個要填的控制項,單擊後可以出現選擇框。現在的問題是,一個房間只有一個寶物,不能被重複預定。所以當買家選擇了寶物類型和房間號之後,在選擇預定日期時,要在日期選擇框給使用者一個提示。比如12月3日051號房間已被預定,現在又有另一位使用者選擇了051號房間,那麼在彈出日期選擇框時,12月3日要置為不可選。如(12月3日顯示為“缺”):
那麼,這樣一個簡單的庫存系統,如何在redis中儲存呢?
二、庫存管理方案(Redis)
最粗暴的想法是,我們的庫存其實就是一個很大的三維數組,第一維寶物類型,第二維房間號,第三維即預定日期。Redis支援5種儲存類型:String,Hash,List,Set,Sorted Set。目前的情境中Hash和Set類型都可以滿足要求,在此我們選擇使用Hash類型做儲存。
Redis的key設定為 寶物類型+房間號(例如 A:205,A代表極品寶物,205為房間號),Redis的value為hash類型,hash key為日期(例如 2016-12-05),hash value為true或false,表示已經被預定或沒有被預定。用圖表示為:
如果A類寶物158房間在12月8日已經被預定,則儲存為
Redis Key —— A:158
Redis Value —— hash table ['2016-12-08' => 1]
三、進階情境&庫存管理方案
你所推出的A類極品寶物很受歡迎,剛推出去不久即被預定出去很多。然而,動輒數十萬元的價格也讓很多有收藏興趣、卻沒那麼富裕的中產階級望而卻步。於是,你又從自己的收藏中挑選出了比A類寶物稍次一些的B類寶物(也稱作“優質寶物”),價格更加親民。
由於B類寶物比A類寶物多一些,你打算換一種玩法,在這300個房間中,每個房間又放入了一個保險箱,這次,你每隔一個小時都會向300個房間的箱中各放入一件B類寶物,沒有被預定的寶物在這一個小時過後會被收回,換成下一個小時的寶物。買家預訂後,按照所預定的小時來取走寶物。對於B類寶物,你的預定系統會多了一個選項,即取貨時間。如:
現在由於多了一個預定條件(取貨時間),那在做庫存儲存的時候,粗暴的方式想一下,庫存其實就是一個大的四維數組。第一維寶物類型,第二維房間號,第三維預定日期,第四維取貨時間。在Redis中怎樣儲存這類寶物呢?
其實仔細想一下,在儲存A類極品寶物的時候,我們在Redis中的儲存是有浪費維度情況的,
當時hashValue只存了一個true表示有預定,這個維度其實是被浪費掉了。考慮到取貨時間全是整點,一整天也就是0至1點,1至2點,……,23至24點共計24種情況,所以我們完全可以使用二進位整數表示被預定的時間。例如1表示0至1點,2表示1至2點,4表示2至3點,……,
8388608 (= 2^23)表示23至24點。多個時間段被預定,只需要將數值取邏輯或操作即可。
這樣,我們的Redis結構變成了這樣子:
例如,B類寶物103房間,12月5日和6日的上午8點至12點被預定,在redis中儲存為
Redis Key —— B:103
Redis Value —— hash table ['2016-12-05' => 3840, '2016-12-06' => 3840]
對於B類寶物,在做新增預定時,需要注意先將原有的hash value取出,和新的預定取貨時間做邏輯或操作,然後再把結果寫回Redis中,而不能像A類寶物一樣直接調用hSet去設定hash value;取消預定時,要注意先將原有的hash value取出,把要取消的時間段從hash value中扣除掉(異或+邏輯與操作),然後重新將剩餘的已預訂取貨時間寫回Redis中,而不能直接調用hDel去刪除。
四、再次進階&庫存管理方案
自從推出了B類寶物之後,你的生意又比以往火爆了許多。於是新的需求又來了,現在有大量的遊客、學生黨等沒什麼豐厚積蓄的人表示對你的寶物非常感興趣,來這個城市旅遊的人都希望帶一些紀念品回去。然而,B類寶物的價格雖然比A類便宜一些,對於這些人來講還是有點貴。於是,你決定把自己餘量最多的實惠寶物(C類寶物)拿出來售賣。
這部分寶物數量是最多的,於是你在這300個房間中,每個房間新增了100個寶箱,專門用於存放C類寶物。這100個寶箱分別被編號為1號,2號,……,100號。同樣的,每天的每個小時,你都會向這300個房間中,每個房間的100個寶箱中分別放入一件C類寶物(也就意味著,整個大樓每小時C類寶物會更新30000件)。如果沒有人預定,則下一個小時寶物更換。終於,這下可以滿足所有人的需求了。
對於C類寶物,你的預定介面成了下面的樣子:
我們又多了一個預定條件。此時,又面臨著庫存儲存的問題。照例,這個庫存其實就是一個大的五維數組,寶物類型、房間號、預定日期、取貨時間、寶箱編號各自佔有一個維度。不過前面我們的Redis各個維度基本上已經佔滿了,這次應該怎麼儲存呢?
這次的Redis庫存儲存必須要結合業務特點來了。首先,寶箱編號和取貨時間這兩個維度,能取的值範圍並不太多,寶箱編號只有100個,只要把hash value變成一個長度為100的數組,數組的每個位置都存有INT類型表示的取貨時間即可。然而hash value只能是string……於是乎,只好做一個數組的序列化操作,讀取的時候再還原序列化回來即可。好在長度只有100,序列化效率並不會成為系統的瓶頸。
例如,C類寶物,12月23日、24日,258房間,97和99號寶箱在11點至13點被預定,則儲存為:
Redis Key —— C:258
Redis Value —— hash table ['2016-12-23' => '[97 => 6144, 99 => 6144]', '2016-12-24' => '[97 => 6144, 99 => 6144]' ]
其中6144用二進位表示為‘110000000000’,hash value為數組序列化以後的字串,實際項目中可以使用json格式。好了,現在Redis對於三種寶物的儲存都有了。
對於C類寶物,在使用者取消預定、新增預定時,同樣不能簡單地調用hSet和hDel進行覆蓋設定和刪除,要取出已經預定的情況,與已經預定的取貨時間做位元運算。
五、儲存最佳化
庫存理論上就是一個多維陣列,我們所做的主要工作就是怎樣把各個維度合理的儲存起來,並能夠方便地進行增加、刪除、查詢操作。從節約使用記憶體的角度講,在最開始還沒有任何人預定的時候,Redis整個可以是空的,對於A類寶物來說,hash value等於false和根本不存在對應的redis key或hash key是等效的。
另外,寶物類型和房間號合起來做redis key,會導致我們在redis中和寶物庫存相關的key的數量比較多,為了方便統一管理這些key,可以再增加一條redis緩衝,專門用來儲存和寶物庫存相關的所有redis key值,如所示。需要注意的是,這次我們並不需要hash資料類型了,set類型就已經足夠,增刪改查複雜度都是O(1)。裡面儲存了所有redis中已經存在的庫存key值。
這麼做的一個好處是,萬一哪天碰到一些特殊情況,需要把所有庫存相關緩衝全部清空的話,我們可以很容易地取出所有的庫存key並做刪除操作。另外一個好處是,給我們提供了繼續擴充的思路……設想一下,現在最複雜的情況是C類寶物,一共5個維度。假設未來,你不再使用一幢樓的300個房間去售賣寶物,而是多幢樓,那麼使用者在下訂單的時候又要多出一個維度——樓棟編號。碰到這種情況,我們完全可以將這個多出來的庫存Key集合退化為樓棟編號來使用,保證了可能出現的更複雜情況下的擴充性。
在做了這次擴充之後,每次新增預定記錄時,需要注意檢測庫存key集合中是否已經存在對應的redis key值,如果不存在需要將redis key值加入庫存key集合中。刪除操作也類似。
六、總結
上面使用了循序漸進的方法講述了一下問題,不過現實的情境中,這三種寶物類型在我們的業務中是同時存在的。上面的設計保持了三種寶物類型儲存上的統一性。如果只考慮A類寶物的話,庫存只有三個維度,其實完全不必使用hash資料類型來儲存,set類型就足夠了。
我們儲存這些預定情況的主要目的,就是為了方便快速地查到庫存衝突情況。比如有人已經定了12月3日,59號房間的A類寶物,那又有另外一個人想預定一樣的日期、房間的A類寶物時,通過記憶體中的庫存查詢,我們可以很方便地告訴客戶,該庫存已經��其他人搶先預定了。
以上就是我在業務中碰到的一個緩衝設計的小問題,不吝賜教!
下面關於Redis的文章您也可能喜歡,不妨參考下:
Ubuntu 14.04下Redis安裝及簡單測試
Redis主從複製基本配置 https://www.bkjia.com/Linux/2015-03/115610.htm
Redis叢集搭建與簡單使用 https://www.bkjia.com/Linux/2017-03/142210.htm
CentOS 7下Redis的安裝與配置 https://www.bkjia.com/Linux/2017-02/140363.htm
Ubuntu 14.04安裝Redis與簡單配置 https://www.bkjia.com/Linux/2017-01/139075.htm
Ubuntu 16.04環境中安裝PHP7.0 Redis擴充 https://www.bkjia.com/Linux/2016-09/135631.htm
Redis 單機&叢集離線安裝部署 https://www.bkjia.com/Linux/2017-03/141403.htm
CentOS 7.0 安裝Redis 3.2.1詳細過程和使用常見問題 https://www.bkjia.com/Linux/2016-09/135071.htm
Ubuntu 16.04環境中安裝PHP7.0 Redis擴充 https://www.bkjia.com/Linux/2016-09/135631.htm
Ubuntu 15.10下Redis叢集部署文檔 https://www.bkjia.com/Linux/2016-06/132340.htm
Redis實戰 中文PDF https://www.bkjia.com/Linux/2016-04/129932.htm
Redis熱遷移實戰總結 https://www.bkjia.com/Linux/2017-02/141083.htm
Redis3.0設定檔詳解 https://www.bkjia.com/Linux/2017-03/141369.htm
本文永久更新連結地址:https://www.bkjia.com/Linux/2018-03/151123.htm