Redis做預(yù)定庫(kù)存緩存功能設(shè)計(jì)使用
最近在自己的工作中,把其中一個(gè)PHP項(xiàng)目的緩存從以前的APC緩存逐漸切換到Redis中,并且根據(jù)Redis所支持的數(shù)據(jù)結(jié)構(gòu)做了庫(kù)存維護(hù)功能。緩存是在業(yè)務(wù)層做的,準(zhǔn)確講應(yīng)該是在MVC模型中Model的ORM里面。主要邏輯就是先查緩存,查不到的話再查數(shù)據(jù)庫(kù)。不過這些不是本文的主要內(nèi)容,下面我把庫(kù)存管理功能的緩存設(shè)計(jì)思路分享一下,希望能帶給大家一些收獲,有不足之處或者有更好方案的,也希望各位多多指教。
一、業(yè)務(wù)背景
為了略去我們公司項(xiàng)目背景,我決定把這次的問題類比成一個(gè)考卷上的問題。至于業(yè)務(wù)細(xì)節(jié),大家也無(wú)需關(guān)注~看題目就可以了:
假設(shè)你是某國(guó)最牛的收藏家,手里有各種價(jià)值連成的寶物。知道有一天,你覺得做收藏太沒意思了,打算把這些寶物賣掉換點(diǎn)現(xiàn)金。
不過把這些值錢的寶貝放在菜市場(chǎng)上賣實(shí)在太low了。在“互聯(lián)網(wǎng)+”時(shí)代,我們當(dāng)然要玩一些不一樣的賣法:在你名下有一棟300個(gè)房間的大樓(編號(hào)為001至300),每個(gè)房間放著一個(gè)密碼鎖保險(xiǎn)箱,在下個(gè)月(12月1日至12月31日)的每一天,你都會(huì)挑選300件最好的“極品寶物”(也稱作A類寶物),分別放入這300個(gè)房間的保險(xiǎn)箱里,每天每個(gè)房間放什么寶物已經(jīng)定好了,所有想買寶物的人必須至少提前一天在網(wǎng)上預(yù)定,到時(shí)候憑借預(yù)定碼自己打開保險(xiǎn)箱取貨。沒有被預(yù)定的寶物將會(huì)被你收回,不再售賣。
要做這樣一個(gè)網(wǎng)絡(luò)預(yù)定系統(tǒng),它的前端界面大概是這樣的:
上圖中三個(gè)要填的控件,單擊后可以出現(xiàn)選擇框?,F(xiàn)在的問題是,一個(gè)房間只有一個(gè)寶物,不能被重復(fù)預(yù)定。所以當(dāng)買家選擇了寶物類型和房間號(hào)之后,在選擇預(yù)定日期時(shí),要在日期選擇框給用戶一個(gè)提示。比如12月3日051號(hào)房間已被預(yù)定,現(xiàn)在又有另一位用戶選擇了051號(hào)房間,那么在彈出日期選擇框時(shí),12月3日要置為不可選。如下圖(12月3日顯示為“缺”):
那么,這樣一個(gè)簡(jiǎn)單的庫(kù)存系統(tǒng),如何在redis中存儲(chǔ)呢?
二、庫(kù)存管理方案(Redis)
最粗暴的想法是,我們的庫(kù)存其實(shí)就是一個(gè)很大的三維數(shù)組,第一維寶物類型,第二維房間號(hào),第三維即預(yù)定日期。Redis支持5種存儲(chǔ)類型:String,Hash,List,Set,Sorted Set。目前的場(chǎng)景中Hash和Set類型都可以滿足要求,在此我們選擇使用Hash類型做存儲(chǔ)。
Redis的key設(shè)置為 寶物類型+房間號(hào)(例如 A:205,A代表極品寶物,205為房間號(hào)),Redis的value為hash類型,hash key為日期(例如 2016-12-05),hash value為true或false,表示已經(jīng)被預(yù)定或沒有被預(yù)定。用圖表示為:
如果A類寶物158房間在12月8日已經(jīng)被預(yù)定,則存儲(chǔ)為
Redis Key —— A:158
Redis Value —— hash table ['2016-12-08' => 1]
三、進(jìn)階場(chǎng)景&庫(kù)存管理方案
你所推出的A類極品寶物很受歡迎,剛推出去不久即被預(yù)定出去很多。然而,動(dòng)輒數(shù)十萬(wàn)元的價(jià)格也讓很多有收藏興趣、卻沒那么富裕的中產(chǎn)階級(jí)望而卻步。于是,你又從自己的收藏中挑選出了比A類寶物稍次一些的B類寶物(也稱作“優(yōu)質(zhì)寶物”),價(jià)格更加親民。
由于B類寶物比A類寶物多一些,你打算換一種玩法,在這300個(gè)房間中,每個(gè)房間又放入了一個(gè)保險(xiǎn)箱,這次,你每隔一個(gè)小時(shí)都會(huì)向300個(gè)房間的箱中各放入一件B類寶物,沒有被預(yù)定的寶物在這一個(gè)小時(shí)過后會(huì)被收回,換成下一個(gè)小時(shí)的寶物。買家預(yù)訂后,按照所預(yù)定的小時(shí)來(lái)取走寶物。對(duì)于B類寶物,你的預(yù)定系統(tǒng)會(huì)多了一個(gè)選項(xiàng),即取貨時(shí)間。如下圖:
現(xiàn)在由于多了一個(gè)預(yù)定條件(取貨時(shí)間),那在做庫(kù)存存儲(chǔ)的時(shí)候,粗暴的方式想一下,庫(kù)存其實(shí)就是一個(gè)大的四維數(shù)組。第一維寶物類型,第二維房間號(hào),第三維預(yù)定日期,第四維取貨時(shí)間。在Redis中怎樣存儲(chǔ)這類寶物呢?
其實(shí)仔細(xì)想一下,在存儲(chǔ)A類極品寶物的時(shí)候,我們?cè)赗edis中的存儲(chǔ)是有浪費(fèi)維度的情況的,
當(dāng)時(shí)hashValue只存了一個(gè)true表示有預(yù)定,這個(gè)維度其實(shí)是被浪費(fèi)掉了??紤]到取貨時(shí)間全是整點(diǎn),一整天也就是0至1點(diǎn),1至2點(diǎn),……,23至24點(diǎn)共計(jì)24種情況,所以我們完全可以使用二進(jìn)制整數(shù)表示被預(yù)定的時(shí)間。例如1表示0至1點(diǎn),2表示1至2點(diǎn),4表示2至3點(diǎn),……,
8388608 (= 2^23)表示23至24點(diǎn)。多個(gè)時(shí)間段被預(yù)定,只需要將數(shù)值取邏輯或操作即可。
這樣,我們的Redis結(jié)構(gòu)變成了這樣子:
例如,B類寶物103房間,12月5日和6日的上午8點(diǎn)至12點(diǎn)被預(yù)定,在redis中存儲(chǔ)為
Redis Key —— B:103
Redis Value —— hash table ['2016-12-05' => 3840, '2016-12-06' => 3840]
對(duì)于B類寶物,在做新增預(yù)定時(shí),需要注意先將原有的hash value取出,和新的預(yù)定取貨時(shí)間做邏輯或操作,然后再把結(jié)果寫回Redis中,而不能像A類寶物一樣直接調(diào)用hSet去設(shè)置hash value;取消預(yù)定時(shí),要注意先將原有的hash value取出,把要取消的時(shí)間段從hash value中扣除掉(異或+邏輯與操作),然后重新將剩余的已預(yù)訂取貨時(shí)間寫回Redis中,而不能直接調(diào)用hDel去刪除。
四、再次進(jìn)階&庫(kù)存管理方案
自從推出了B類寶物之后,你的生意又比以往火爆了許多。于是新的需求又來(lái)了,現(xiàn)在有大量的游客、學(xué)生黨等沒什么豐厚積蓄的人表示對(duì)你的寶物非常感興趣,來(lái)這個(gè)城市旅游的人都希望帶一些紀(jì)念品回去。然而,B類寶物的價(jià)格雖然比A類便宜一些,對(duì)于這些人來(lái)講還是有點(diǎn)貴。于是,你決定把自己余量最多的實(shí)惠寶物(C類寶物)拿出來(lái)售賣。
這部分寶物數(shù)量是最多的,于是你在這300個(gè)房間中,每個(gè)房間新增了100個(gè)寶箱,專門用于存放C類寶物。這100個(gè)寶箱分別被編號(hào)為1號(hào),2號(hào),……,100號(hào)。同樣的,每天的每個(gè)小時(shí),你都會(huì)向這300個(gè)房間中,每個(gè)房間的100個(gè)寶箱中分別放入一件C類寶物(也就意味著,整個(gè)大樓每小時(shí)C類寶物會(huì)更新30000件)。如果沒有人預(yù)定,則下一個(gè)小時(shí)寶物更換。終于,這下可以滿足所有人的需求了。
對(duì)于C類寶物,你的預(yù)定界面成了下面的樣子:
我們又多了一個(gè)預(yù)定條件。此時(shí),又面臨著庫(kù)存存儲(chǔ)的問題。照例,這個(gè)庫(kù)存其實(shí)就是一個(gè)大的五維數(shù)組,寶物類型、房間號(hào)、預(yù)定日期、取貨時(shí)間、寶箱編號(hào)各自占有一個(gè)維度。不過前面我們的Redis各個(gè)維度基本上已經(jīng)占滿了,這次應(yīng)該怎么存儲(chǔ)呢?
這次的Redis庫(kù)存存儲(chǔ)必須要結(jié)合業(yè)務(wù)特點(diǎn)來(lái)了。首先,寶箱編號(hào)和取貨時(shí)間這兩個(gè)維度,能取的值范圍并不太多,寶箱編號(hào)只有100個(gè),只要把hash value變成一個(gè)長(zhǎng)度為100的數(shù)組,數(shù)組的每個(gè)位置都存有INT類型表示的取貨時(shí)間即可。然而hash value只能是string……于是乎,只好做一個(gè)數(shù)組的序列化操作,讀取的時(shí)候再反序列化回來(lái)即可。好在長(zhǎng)度只有100,序列化效率并不會(huì)成為系統(tǒng)的瓶頸。
例如,C類寶物,12月23日、24日,258房間,97和99號(hào)寶箱在11點(diǎn)至13點(diǎn)被預(yù)定,則存儲(chǔ)為:
Redis Key —— B:103
Redis Value —— hash table ['2016-12-05' => 3840, '2016-12-06' => 3840]
其中6144用二進(jìn)制表示為‘110000000000’,hash value為數(shù)組序列化以后的字符串,實(shí)際項(xiàng)目中可以使用json格式。好了,現(xiàn)在Redis對(duì)于三種寶物的存儲(chǔ)都有了。
對(duì)于C類寶物,在用戶取消預(yù)定、新增預(yù)定時(shí),同樣不能簡(jiǎn)單地調(diào)用hSet和hDel進(jìn)行覆蓋設(shè)置和刪除,要取出已經(jīng)預(yù)定的情況,與已經(jīng)預(yù)定的取貨時(shí)間做位運(yùn)算。
五、存儲(chǔ)優(yōu)化
庫(kù)存理論上就是一個(gè)多維數(shù)組,我們所做的主要工作就是怎樣把各個(gè)維度合理的存儲(chǔ)起來(lái),并能夠方便地進(jìn)行增加、刪除、查詢操作。從節(jié)約使用內(nèi)存的角度講,在最開始還沒有任何人預(yù)定的時(shí)候,Redis整個(gè)可以是空的,對(duì)于A類寶物來(lái)說(shuō),hash value等于false和根本不存在對(duì)應(yīng)的redis key或hash key是等效的。
另外,寶物類型和房間號(hào)合起來(lái)做redis key,會(huì)導(dǎo)致我們?cè)趓edis中和寶物庫(kù)存相關(guān)的key的數(shù)量比較多,為了方便統(tǒng)一管理這些key,可以再增加一條redis緩存,專門用來(lái)存儲(chǔ)和寶物庫(kù)存相關(guān)的所有redis key值,如下圖所示。需要注意的是,這次我們并不需要hash數(shù)據(jù)類型了,set類型就已經(jīng)足夠,增刪改查復(fù)雜度都是O(1)。里面存儲(chǔ)了所有redis中已經(jīng)存在的庫(kù)存key值。
這么做的一個(gè)好處是,萬(wàn)一哪天碰到一些特殊情況,需要把所有庫(kù)存相關(guān)緩存全部清空的話,我們可以很容易地取出所有的庫(kù)存key并做刪除操作。另外一個(gè)好處是,給我們提供了繼續(xù)擴(kuò)展的思路……設(shè)想一下,現(xiàn)在最復(fù)雜的情況是C類寶物,一共5個(gè)維度。假設(shè)未來(lái),你不再使用一幢樓的300個(gè)房間去售賣寶物,而是多幢樓,那么用戶在下訂單的時(shí)候又要多出一個(gè)維度——樓棟編號(hào)。碰到這種情況,我們完全可以將這個(gè)多出來(lái)的庫(kù)存Key集合退化為樓棟編號(hào)來(lái)使用,保證了可能出現(xiàn)的更復(fù)雜情況下的擴(kuò)展性。
在做了這次擴(kuò)展之后,每次新增預(yù)定記錄時(shí),需要注意檢測(cè)庫(kù)存key集合中是否已經(jīng)存在對(duì)應(yīng)的redis key值,如果不存在需要將redis key值加入庫(kù)存key集合中。刪除操作也類似。
六、總結(jié)
上面使用了循序漸進(jìn)的方法講述了一下問題,不過現(xiàn)實(shí)的場(chǎng)景中,這三種寶物類型在我們的業(yè)務(wù)中是同時(shí)存在的。上面的設(shè)計(jì)保持了三種寶物類型存儲(chǔ)上的統(tǒng)一性。如果只考慮A類寶物的話,庫(kù)存只有三個(gè)維度,其實(shí)完全不必使用hash數(shù)據(jù)類型來(lái)存儲(chǔ),set類型就足夠了。
我們存儲(chǔ)這些預(yù)定情況的主要目的,就是為了方便快速地查到庫(kù)存沖突情況。比如有人已經(jīng)定了12月3日,59號(hào)房間的A類寶物,那又有另外一個(gè)人想預(yù)定一樣的日期、房間的A類寶物時(shí),通過內(nèi)存中的庫(kù)存查詢,我們可以很方便地告訴客戶,該庫(kù)存已經(jīng)被其他人搶先預(yù)定了。
以上就是我在業(yè)務(wù)中碰到的一個(gè)緩存設(shè)計(jì)的小問題,不吝賜教!
以上就是Redis做預(yù)定庫(kù)存緩存功能設(shè)計(jì)使用的詳細(xì)內(nèi)容,更多關(guān)于Redis預(yù)定庫(kù)存緩存設(shè)計(jì)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Redis設(shè)置Hash數(shù)據(jù)類型的過期時(shí)間
在Redis中,我們可以使用Hash數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)一組鍵值對(duì),而有時(shí)候,我們可能需要設(shè)置這些鍵值對(duì)的過期時(shí)間,本文主要介紹了Redis設(shè)置Hash數(shù)據(jù)類型的過期時(shí)間,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01Redis+Caffeine實(shí)現(xiàn)分布式二級(jí)緩存組件實(shí)戰(zhàn)教程
這篇文章主要介紹了Redis+Caffeine實(shí)現(xiàn)分布式二級(jí)緩存組件實(shí)戰(zhàn)教程,介紹了分布式二級(jí)緩存的優(yōu)勢(shì),使用組件的方法,通過示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08