緩存穿透的問(wèn)題
通常使用的緩存都是應(yīng)用先檢查緩存中是否存在。如果存在,則直接返回緩存;如果不存在,則查詢數(shù)據(jù)庫(kù),將結(jié)果緩存并返回。
但是查詢的是一個(gè)不存在的數(shù)據(jù),就會(huì)造成每一次請(qǐng)求都查詢DB,這樣緩存就失去了意義。
為了解決這個(gè)問(wèn)題,通常使用特殊字符串標(biāo)記無(wú)用的緩存,比如"EmptyCache",然后在應(yīng)用層做相應(yīng)處理,解決此問(wèn)題。但是這種方式顯得比較粗暴:
- 需要在反序列化過(guò)程中,將特殊字符串排除掉,同時(shí)需要將不存在的數(shù)據(jù)標(biāo)記出來(lái),避免再查詢DB。
- 如果需要處理不同類型的數(shù)據(jù)的緩存穿透問(wèn)題,是不是多加幾種特殊字符傳。
因此,我們可以引入NullObject模式來(lái)解決此問(wèn)題。它的作用在于提供一個(gè)對(duì)象給指定的類型,用以代替這個(gè)對(duì)象為空的情況。
更具體的信息,可以參考此鏈接:https://en.wikipedia.org/wiki/Null_object
新的方案:以券(Ticket)為例
步驟一、引入抽象類AbstractNullObject
public abstract class AbstractNullObject{
private Integer id;
public void markNullObject(){
this.id = -1;
}
public void isNullObject(){
return id!=null && id<0;
}
}
一般在設(shè)計(jì)數(shù)據(jù)庫(kù)表時(shí),都有id字段,而且id字段是自增的整數(shù)。我們可以復(fù)用此id字段,并約定一個(gè)規(guī)則:id小0的對(duì)象,都是NullObject。
步驟二、讓Ticket繼承能夠AbstractNullObject,并添加createNullObject函數(shù)
public class Ticket extends AbstractNullObject{
public static Ticket createNullObject(String ticketCode){
Ticket ticket =new Ticket();
ticket.setTicketCode(ticketCode):
ticket.markNullObject();
}
}
步驟三、更新緩存的查詢流程
舊流程 | 新流程 |
---|---|
1. 根據(jù)TicketCodes,查詢緩存,并將結(jié)果加入結(jié)果集。 2. 根據(jù)TicketCodes和結(jié)果集,找出不存在的TicketCode,然后到db查詢,將結(jié)果加入結(jié)果集,并寫(xiě)入緩存。 3. 返回結(jié)果集合。 |
1. 根據(jù)TicketCodes,查詢緩存,并將結(jié)果加入結(jié)果集。 2. 根據(jù)TicketCodes和結(jié)果集,找出不存在的TicketCode,然后到db查詢,將結(jié)果加入結(jié)果集,并寫(xiě)入緩存。 3. 根據(jù)TicketCodes和結(jié)果集,再次不存在的TicketCode,為這些code創(chuàng)建NullObject,并寫(xiě)入緩存。 4. 過(guò)濾結(jié)果集,剔除NullObject。 5. 返回結(jié)果集。 |
對(duì)比新老流程,新流程增加了步驟3、4。
步驟3的作用:為不存在的TicketCode,創(chuàng)建NullObject,寫(xiě)入緩存。下次再次查詢時(shí),緩存就有數(shù)據(jù),DB也不會(huì)查詢。
步驟4的作用:NullObject是可以正常被反序列化,但對(duì)調(diào)用方來(lái)說(shuō)是無(wú)效的數(shù)據(jù),需要剔除。
總結(jié)
帶來(lái)的好處:
- 如果想讓Sku,Product避免緩存穿透的問(wèn)題,只要繼承NullObject接口,就能復(fù)用已有的成果。
- 在redis反序列化過(guò)程,沒(méi)有特殊處理。
- 在原有的查詢流程追加行為,沒(méi)有破壞性的改造,成本低。
與老方案的差異:
序列化NullObject產(chǎn)生的字符串,攜帶的信息量遠(yuǎn)大于特殊字符傳,而且反序列化后,可以判斷是否為NullObject。