背景引入
最近需要實(shí)現(xiàn)一個(gè)功能须床,關(guān)于頁(yè)面廣告自動(dòng)配置的铐料,如支付寶的支付完成頁(yè)。這篇隨筆是記錄對(duì)這個(gè)需求從分析到實(shí)現(xiàn)以及優(yōu)化的過程豺旬,以免以后忘記钠惩。如果你最近也要做著樣的功能,希望對(duì)你有所啟發(fā)族阅。
需求描述
某些頁(yè)面需要配置廣告或活動(dòng)宣傳圖篓跛,廣告或活動(dòng)需滿足隨時(shí)上下線、過期自動(dòng)下線及到時(shí)自動(dòng)上線坦刀。
如:現(xiàn)在時(shí)間2019-2-22 16:16:13愧沟,要在支付完成頁(yè)面配置領(lǐng)獎(jiǎng)活動(dòng),活動(dòng)要在2019-3-10 00:00:00準(zhǔn)時(shí)上線求泰,在2019-3-30 23:59:59結(jié)束活動(dòng)央渣。
所以要的效果是,在活動(dòng)上線前的任意時(shí)刻配置完活動(dòng)后渴频,頁(yè)面到時(shí)間自動(dòng)上線這個(gè)活動(dòng)芽丹。
也可能會(huì)是其他的多個(gè)活動(dòng)或廣告,每個(gè)頁(yè)面廣告的個(gè)數(shù)可變卜朗,不同上下線時(shí)間可不同拔第,其他頁(yè)面也需要實(shí)現(xiàn)這樣的功能,頁(yè)面與頁(yè)面之間的活動(dòng)不一定一樣场钉。
需求分析
需求簡(jiǎn)單的幾句話蚊俺,那么我們來具體的分析一下。
提取關(guān)鍵點(diǎn)
- 廣告或活動(dòng)宣傳圖
- 隨時(shí)上下線逛万、過期自動(dòng)下線及到時(shí)自動(dòng)上線
- 每個(gè)頁(yè)面廣告的個(gè)數(shù)可變
- 不同廣告上下線時(shí)間可不同
- 頁(yè)面與頁(yè)面之間的活動(dòng)不一定一樣
數(shù)據(jù)庫(kù)分析
1泳猬、【廣告或活動(dòng)宣傳圖】
要為不同頁(yè)面設(shè)置不同的廣告,有的頁(yè)面廣告可能一樣宇植,也就是廣告會(huì)復(fù)用得封,所有要有廣告表。
2指郁、【每個(gè)頁(yè)面廣告的個(gè)數(shù)可變】【不同廣告上下線時(shí)間可不同】【頁(yè)面與頁(yè)面之間的活動(dòng)不一定一樣】
頁(yè)面可配置多個(gè)廣告忙上,所有要有頁(yè)面配置表,以及廣告和頁(yè)面的關(guān)系表闲坎,即頁(yè)面廣告表疫粥。
頁(yè)面配置表主要配置頁(yè)面的廣告?zhèn)€數(shù)茬斧,實(shí)現(xiàn)【每個(gè)頁(yè)面廣告的個(gè)數(shù)可變】,頁(yè)面廣告表主要配置頁(yè)面的每個(gè)廣告上下線時(shí)間梗逮,實(shí)現(xiàn)【不同廣告上下線時(shí)間可不同】
簡(jiǎn)單分析后得出如下表結(jié)構(gòu):廣告表adv项秉,頁(yè)面配置表page_config,頁(yè)面廣告表page_adv
思考
這些頁(yè)面配置的廣告在一段時(shí)間內(nèi)是不會(huì)變的库糠,如果頁(yè)面請(qǐng)求次數(shù)較多伙狐,廣告查詢次數(shù)就會(huì)很頻繁涮毫,對(duì)數(shù)據(jù)庫(kù)造成不必要的壓力瞬欧。所以可以引入緩存,降低數(shù)據(jù)庫(kù)請(qǐng)求次數(shù)罢防,緩解數(shù)據(jù)庫(kù)壓力艘虎。這里使用的Redis。
何時(shí)入緩存咒吐?
可以選擇在服務(wù)啟動(dòng)時(shí)異步把已在上下線時(shí)間區(qū)間內(nèi)的廣告先加載至緩存野建,或選擇在請(qǐng)求時(shí)取緩存,緩存內(nèi)沒有時(shí)再查庫(kù)然后放緩存恬叹。緩存時(shí)間視情況而定候生。
這里選擇的是,項(xiàng)目啟動(dòng)時(shí)異步把符合條件的頁(yè)面廣告配置信息存入Redis绽昼,那些還沒到指定時(shí)間的先不放Redis唯鸭,等到訪問頁(yè)面加載廣告時(shí),先查Redis硅确,若無(wú)則按條件(>=nowtime)查庫(kù)目溉,查到后存Redis。
在接口中拿到廣告配置信息后菱农,判斷當(dāng)前時(shí)間是否在配置的時(shí)間區(qū)間內(nèi)缭付,由于一個(gè)頁(yè)面配置多個(gè)廣告,不同廣告時(shí)間也不同循未,所以要迭代陷猫,把符合的返回,有過期的就做標(biāo)記的妖,然后把整個(gè)頁(yè)面的配置信息在Redis里刪除绣檬。
(或者不選擇在啟動(dòng)時(shí)加載,就在用戶請(qǐng)求時(shí)加入緩存羔味,但是下面的第1步的方法在刷新加載時(shí)會(huì)用到河咽,故不能刪)
具體實(shí)現(xiàn)
第1步、查詢頁(yè)面廣告配置信息存入Redis
這一步是為了項(xiàng)目啟動(dòng)時(shí)調(diào)這個(gè)方法赋元,把符合條件的頁(yè)面廣告配置信息存入Redis忘蟹,并且刷新加載時(shí)也回調(diào)這個(gè)方法飒房。
a、查詢所有pageId
SELECT pageId FROM page_config page_adv WHERE nowtime<=endtime AND GROUP BY pageId
兩個(gè)表內(nèi)連接媚值,得List<pageId>狠毯,得到的都是配置的有廣告的并且廣告還沒過期的pageId。
b褥芒、查詢pegeId對(duì)應(yīng)的廣告圖片及跳轉(zhuǎn)鏈接
SELECT 字段名 FROM page_adv adv WHERE begintime<=nowtime<=endtime AND pageId={#pageId}
然后把查到的配置信息List<adv>(為空時(shí)不做操作)嚼松,以pageId為KEY放入緩存。
第2步锰扶、給前端寫接口查詢頁(yè)面廣告
按標(biāo)準(zhǔn)的控制層献酗,業(yè)務(wù)層,數(shù)據(jù)訪問層寫坷牛,第一步中的邏輯就是在業(yè)務(wù)層完成的罕偎。
控制層:
控制層接參pageId,調(diào)用業(yè)務(wù)層查詢對(duì)應(yīng)頁(yè)面配置的廣告信息京闰,判空颜及,直接返回狀態(tài)碼0,即無(wú)廣告前端不展示蹂楣。
不為空就根據(jù)業(yè)務(wù)邏輯處理數(shù)據(jù)(如img的URL加域名)俏站,然后返回狀態(tài)碼1,前端展示廣告痊土。
這里控制層還可以加邏輯肄扎,迭代廣告list,把當(dāng)前時(shí)間在廣告起始時(shí)間內(nèi)的返回施戴,不在的不返回反浓,并且只要有一個(gè)廣告過期,就把這個(gè)頁(yè)面的廣告list緩存清掉赞哗。這個(gè)邏輯是把過期的清掉雷则。
業(yè)務(wù)層:
先取緩存,沒有再查庫(kù)判斷不為空(本頁(yè)面配置的有廣告)肪笋,放入緩存(pageId為KEY)月劈,然后返回。
數(shù)據(jù)訪問層:
SQL:
SELECT 字段名 FROM page_config adv page_adv WHERE begintime<=nowtime<=endtime AND pageId=#{pageId}
三表聯(lián)查藤乙,根據(jù)pageId查詢當(dāng)前頁(yè)面配置的廣告活動(dòng)信息(已在廣告活動(dòng)時(shí)間內(nèi))
第3步猜揪、刷新加載
為什么使用刷新加載?
因?yàn)橛羞@樣的場(chǎng)景:給頁(yè)面A配置了一個(gè)廣告(當(dāng)前時(shí)間在廣告的起始時(shí)間內(nèi))坛梁,那么這個(gè)頁(yè)面的廣告已經(jīng)在緩存里了而姐,假如此時(shí)A頁(yè)面要新加一個(gè)廣告,在后臺(tái)配置后如果不做其他操作划咐,這個(gè)廣告不會(huì)顯示(假設(shè)緩存時(shí)間較長(zhǎng)拴念,為一天)钧萍,因?yàn)閹?kù)更新了,緩存沒有同步更新政鼠。
解決方案
使用Redis的發(fā)布訂閱機(jī)制實(shí)現(xiàn)緩存的刷新加載风瘦,使新配置的廣告及時(shí)能夠顯示。
刷新加載的回調(diào)方法即第1步中的方法公般。
進(jìn)一步優(yōu)化
想一想万搔,目前的實(shí)現(xiàn)存在什么問題?
存在的問題
假如有頁(yè)面需要配置廣告官帘,但是還沒有配(前端已經(jīng)開發(fā)完上線瞬雹,每次都會(huì)調(diào)接口查廣告信息),那么數(shù)據(jù)庫(kù)肯定查不到遏佣,緩存也沒有挖炬。如果這個(gè)頁(yè)面訪問量很大揽浙,那么緩存沒命中就查庫(kù)状婶,這樣對(duì)庫(kù)的壓力就會(huì)很大,這就是緩存穿透馅巷,請(qǐng)求上來了很容易擊垮數(shù)據(jù)庫(kù)膛虫。那怎么辦呢?
解決方案
當(dāng)頁(yè)面沒有配置廣告時(shí)钓猬,在緩存存標(biāo)志稍刀,查詢時(shí)先看標(biāo)志,在決定是否往下走敞曹。
具體方案
這時(shí)账月,上面的第1步就要改了。
1澳迫、首先改第1步的步驟a的SQL局齿,把所有的pageId都查詢出來。
使用左連接
SELECT pageId FROM page_config LEFT JOIN page_adv ON ... GROUP BY pageId
或者干脆查page_config
SELECT pageId FROM page_config
目的是把已在page_config表中配置橄登,但關(guān)系表中page_adv未配置廣告的pageId也查出來抓歼,這樣才能給未配置廣告的pageId在緩存里放標(biāo)志
2、第1步的步驟b的SQL改為
SELECT 字段名 FROM page_adv adv WHERE nowtime<=endtime AND pageId={#pageId}
然后把查到的配置信息放入緩存之前
為什么只判斷小于結(jié)束時(shí)間
因?yàn)槿绻擁?yè)面配置的廣告開始時(shí)間大于當(dāng)前時(shí)間拢锹,那么這個(gè)是查不到的谣妻,會(huì)被處理為DATABASE_IS_NULL,如果在這個(gè)標(biāo)志還沒失效之前就到了配置的開始時(shí)間了卒稳,那么這個(gè)廣告不會(huì)被展示蹋半。所有要讓未到開始時(shí)間的也放入緩存,然后讓控制層去判斷在不在時(shí)間區(qū)間充坑。
3减江、所以要在第2步也修改一下
在業(yè)務(wù)層里取緩存中的廣告列表之前闻蛀,先從緩存取pageId+"EMPTY_FLAG"的value判斷為"DB_IS_NULL"直接返回空,這樣就能解決緩存穿透的問題了您市。
繼續(xù)修改第2步的業(yè)務(wù)層觉痛,查庫(kù)的SQL同樣要改:
SELECT 字段名 FROM page_config adv page_adv WHERE nowtime<=endtime AND pageId=#{pageId}
然后判斷為空的話,同上面的黃字那樣處理茵休。
4薪棒、最后,第3步的刷新加載調(diào)的是第1步的方法榕莺,不用改俐芯。
當(dāng)然這個(gè)緩存穿透的優(yōu)化方案只是其中一種。還可以這樣:
1钉鸯、控制層攔截:根據(jù)pageId查詢page_adv表吧史,查不到說明沒配置,直接返回唠雕。
2贸营、page_config 表增加字段,表示當(dāng)前頁(yè)面已經(jīng)配置的廣告?zhèn)€數(shù)岩睁,默認(rèn)0钞脂,每配置一個(gè)該字段加1,把大于0的pageId緩存起來捕儒,調(diào)接口時(shí)前判斷在不在緩存里冰啃。
總結(jié):
實(shí)現(xiàn)這個(gè)功能并不是太難,主要用到了Redis的緩存技術(shù)刘莹,Redis發(fā)布訂閱機(jī)制阎毅,關(guān)鍵就是細(xì)節(jié)的把控,以及緩存穿透的處理点弯。
END
著作權(quán)歸作者所有扇调。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處蒲拉。