一略吨、背景
社區(qū)收藏業(yè)務(wù)是一個典型的讀多寫少的場景屏富,社區(qū)各種核心Feeds流都需要依賴用戶是否收藏的數(shù)據(jù)判斷驯耻,早期緩存設(shè)計(jì)時(shí)由于流量不是很大发皿,未體現(xiàn)出明顯的問題崔慧,近期通過監(jiān)控平臺等相關(guān)手段發(fā)現(xiàn)了相關(guān)的一些問題,因此我們針對這些問題對緩存做了重構(gòu)設(shè)計(jì)穴墅,以保障收藏業(yè)務(wù)的性能和穩(wěn)定性惶室。
二、問題分析定位
2.1 接口RT偏大
通過監(jiān)控平臺查看「判斷是否收藏接口」的RT在最高在8ms左右玄货,該接口的主要作用是判斷指定單個用戶是否已收藏一批內(nèi)容皇钞,其實(shí)如果緩存命中率高的話,接口RT就應(yīng)該趨近于Redis的RT水平松捉,也就是1-2ms左右夹界。
2.2 Redis&MySQL訪問QPS偏高
通過監(jiān)控平臺可以看到從上游服務(wù)過來的收藏查詢QPS相對訪問Redis緩存的QPS放大了15倍,并且MySQL查詢的最高QPS占上游訪問量接近37%隘世,這說明緩存并沒有很高的命中率可柿,導(dǎo)致回表查詢的概率還是很大。
QPS訪問量見下圖:
Redis訪問量
MySQL訪問量
基于以上分析我們現(xiàn)在有了明確的優(yōu)化切入點(diǎn)丙者,接下來我們來看下具體的找下原因是什么复斥。
接下來我們來看一下偽代碼的實(shí)現(xiàn):
//判斷用戶是否對指定的動態(tài)收藏
func IsLightContent(userId uint64,contentIds []uint64){
index := userId%20
cacheKey := key + "_" + fmt.Sprintf("%d", index)
pipe := redis.GetClient().Pipeline()
for _, item := range contentIds {
InitCache(userId, contentId)
pipe.SisMember(cacheKey, userId)
}
pipe.Exec()
//......
}
//緩存初始化判斷,不存在則初始化數(shù)據(jù)緩存
func InitCache(userId uint64,contentId uint64){
index := userId%20
cacheKey := key + "_" + fmt.Sprintf("%d", index)
ttl,_ := redis.GetClient().TTL(cacheKey)
if ttl <= 0{//key不存在或者未設(shè)置過期時(shí)間
// query from db
// sql := "select userId from trendFav where userId%20 = index and content_id = contentId"
// save to redis
}else{
redis.GetClient().Expire(cacheKey,time.Hour()*48)
}
}
從上面的偽代碼中械媒,我們能夠很清晰的看到目锭,該方法會遍歷內(nèi)容id集合,然后對每個內(nèi)容去查詢緩存下來的用戶集合纷捞,判斷該當(dāng)前用戶是否收藏痢虹。也就是說緩存設(shè)計(jì)是按照內(nèi)容維度和用戶1:N來設(shè)計(jì)的,將單個動態(tài)下所有收藏過內(nèi)容的用戶id查出來緩存起來主儡。并且基于大Key的考慮世分,代碼又將用戶集合分片成20組。這無疑又再次放大了Redis緩存Key的數(shù)量缀辩。并且每個Key都使用TTL命令來判斷是否過期臭埋。這樣一來Redis的QPS和緩存Key就會被放大很多倍。
正是由于分片策略+緩存時(shí)效短臀玄,導(dǎo)致了MySQL查詢的QPS居高不下瓢阴。
三、解決方案
基于以上對問題的分析定位健无,我們思考的解決思路就是一次接口請求降低Redis查詢操作荣恐,盡可能減少放大的情況,初步判斷有如下兩個實(shí)現(xiàn)路徑:
去掉遍歷內(nèi)容查詢,改為一次性查詢
去掉用戶集分片存儲叠穆,改為單Key存儲
上游的調(diào)用參數(shù)用戶和內(nèi)容是一對多的關(guān)系少漆,因此要實(shí)現(xiàn)的Redis查詢也是要滿足一對多的關(guān)系,那么顯而易見我們的緩存應(yīng)該是按照用戶的維度來存儲已經(jīng)收藏過的內(nèi)容集合硼被。
用戶收藏的內(nèi)容比較少的話示损,我們很簡單的就可以從數(shù)據(jù)庫全部查詢出來放在緩存,但如果用戶收藏的內(nèi)容比較多呢嚷硫,那也會可能造成大Key問題检访,如果繼續(xù)分片存儲的話又會回到了原來的方案。我們討論出以下兩種方案:
方案1. 處理大數(shù)據(jù)大部分常規(guī)思路就是要么分片仔掸,要么冷熱分離
因?yàn)闃I(yè)務(wù)邏輯的特點(diǎn)脆贵,推薦流下用戶看到的內(nèi)容絕大部份基本都是一年以內(nèi)的,我們可以緩存用戶一年以內(nèi)的收藏內(nèi)容起暮,這樣就限制了用戶收藏的極端數(shù)量卖氨。如果看到的內(nèi)容發(fā)布超過一年時(shí)間,可以用MySQL直接查詢负懦,這種場景的case概率是很小的筒捺。但仔細(xì)考慮了下實(shí)現(xiàn),這個需要依賴業(yè)務(wù)方密似,我們需要去查詢內(nèi)容的發(fā)布時(shí)間,以此來判斷是否在我們的緩存內(nèi)葫盼,這樣會加重整個接口的邏輯残腌,反而得不償失,因此該思路很快就被否定了贫导。
方案2. 既然不能依賴第三方抛猫,就是要從自身擁有的信息上,來能夠緩存一部分最熱的數(shù)據(jù)孩灯,使得查詢能夠大范圍落到這些數(shù)據(jù)
我們目前只有內(nèi)容id闺金,而內(nèi)容id都是純數(shù)字,數(shù)字本身的話可以按照大小來排列峰档。業(yè)務(wù)查詢本身都是最近一段時(shí)間的內(nèi)容败匹,所以查詢的內(nèi)容id都是近期較大的id。那我們可以按照內(nèi)容id降序排列讥巡,取用戶收藏過的若干條數(shù)據(jù)來緩存掀亩。只要查詢的id都比緩存最小的id大,那么我們就可以只通過緩存來判斷出用戶是否收藏這些內(nèi)容了欢顷。
示例:
初始化緩存時(shí)我們按照內(nèi)容id降序排列槽棍,拿到前5000個內(nèi)容id:
如果查詢結(jié)果不滿5000,那么這個用戶緩存了全部收藏記錄,此時(shí)小緩存的內(nèi)容id為0
如果大于等于5000炼七,說明還有部分未緩存的記錄缆巧,此時(shí)最小緩存的內(nèi)容id為第5000個內(nèi)容ID
等到查詢判斷時(shí),將查詢的內(nèi)容id數(shù)組和緩存的最小內(nèi)容id對比豌拙,如果全部大于陕悬,則說明都在緩存范圍內(nèi),如果有小于姆蘸,則是超過緩存范圍墩莫,屆時(shí)單獨(dú)去數(shù)據(jù)庫判斷,當(dāng)然這種概率在業(yè)務(wù)上的發(fā)生幾率是比較小的逞敷。
這里緩存的數(shù)量的抉擇顯得尤為重要狂秦,如果太小,那緩存的命中率不高推捐,導(dǎo)致MySQL回表查詢概率變大裂问,如果太大,則初始化時(shí)比較耗費(fèi)時(shí)間牛柒,或產(chǎn)生大Key問題堪簿。經(jīng)過分析線上數(shù)據(jù),目前以5000這個數(shù)字能夠比較好的權(quán)衡皮壁。
下面是查詢緩存判斷流程圖:
緩存方式由原來的set結(jié)構(gòu)椭更,改為Hash結(jié)構(gòu),TTL延長到7 * 24 hour蛾魄。
這樣一來平委,原來的獨(dú)立調(diào)用的TTL和sismember命令洼畅,可以合并成一個Hmget命令,減少了一半的Redis訪問次數(shù),這個改進(jìn)收益是相當(dāng)可觀的劳曹。
四臊泰、優(yōu)化成果
截止本文撰寫時(shí)舞骆,我們對收藏的功能進(jìn)行了優(yōu)化改造并上線曹步,取得了很不錯的進(jìn)展。所有數(shù)據(jù)為最近7天的數(shù)據(jù)4.14 - 4.20,優(yōu)化效果在4.15號17點(diǎn)左右開始魔市。
4.1 RPC接口響應(yīng)RT降低
1 IsCollectionContent
RPC接口主届,判斷動態(tài)是否緩存。平均RT提高了接近3倍待德。并且RT比較穩(wěn)定
4.2 Redis負(fù)載降低
1 TTL 查詢
查詢Key有效期岂膳,用來判斷延長Key有效期。QPS直接降到0
2 SISMEMBER查詢
原來舊的收藏緩存查詢磅网,已經(jīng)改為HMGET查詢QPS降低到0
3 HMGET查詢
新的收藏緩存查詢QPS數(shù)量和上游過來查詢的QPS正好能對應(yīng)上
4 Redis 內(nèi)存降低
新的緩存較舊緩存在占用內(nèi)存和Key數(shù)量這2個指標(biāo)均降低了3倍左右
4.3 MySQL負(fù)載降低
1 content_collection表select查詢降低
QPS降低了24倍左右并且保持在一個比較穩(wěn)定的水位
2 MySQL連接并發(fā)數(shù)降低
查詢QPS的減少也降低了并發(fā)連接數(shù)谈截,大概降低了3倍左右,最終也降低了等待連接次數(shù)
五、總結(jié)
經(jīng)過對本次問題的分析和解決簸喂,不難看出一個良好的緩存設(shè)計(jì)對于服務(wù)來說是多么的重要毙死。好的緩存設(shè)計(jì)不僅能夠提升性能,同時(shí)可以降低資源使用喻鳄,整體提升了資源利用率扼倘。同時(shí)下游的流量和上游基本持平,在流量上升時(shí)除呵,不會對下游造成很大的壓力再菊,這樣服務(wù)整體的抗并發(fā)能力也提升了很多。
*文/Sky
關(guān)注得物技術(shù)颜曾,每周一三五晚18:30更新技術(shù)干貨
要是覺得文章對你有幫助的話纠拔,歡迎評論轉(zhuǎn)發(fā)點(diǎn)贊~