某活動需求,每天10點對昨天參加某活動的用戶進(jìn)行推送提醒。開發(fā)人員使用redis存儲每天參加活動的用戶,通過ZRANGEBYSCORE命令獲取目標(biāo)用戶進(jìn)行提醒,提醒完后使用ZREMRANGEBYSCORE命令從redis中清除這批用戶。某一天ZRANGEBYSCORE、ZREMRANGEBYSCORE均出現(xiàn)了慢日志報警,排查發(fā)現(xiàn)這一天參加該活動的用戶約有5萬。 案例中使用了redis的sortedset來儲存用戶信息,其中value是用戶的賬號、score是用戶參加活動的時間,由于ZRANGEBYSCORE和ZREMRANGEBYSCORE命令的時間復(fù)雜度是 O(log(N) M),其中M是操作的元素個數(shù),N是集合元素總數(shù),本例中當(dāng)用戶數(shù)量為5萬時出現(xiàn)慢日志??梢酝ㄟ^縮小每次查詢的集合數(shù)量,可以將一天分成多段,分批次查詢,比如把查24小時范圍的用戶改為查4小時范圍的用戶,分別查6次處理即可。 Q 如果用戶參加活動的時間很集中,在某一 個時間段(比如晚18點到22點)查出來的 數(shù)量還是特別多怎么辦? A 可以把粒度分得更細(xì)一些比如1小時或者30分鐘,如果確定用戶參加活動集中在某個時間點,可以考慮使用ZSCAN遍歷操作并刪除。另外,對于目標(biāo)時間范圍有確定的首尾元素時,還可以通過ZRANK命令查出元素的位置,通過 ZRANGE 以及ZREMRANGEBYRANK來進(jìn)行查詢和刪除操作,這樣每次操作可以控制操作數(shù)量,有效避免慢日志。 使用 sortedset、set、list、hash等集合類的O(N)操作時要評估當(dāng)前元素個數(shù)的規(guī)模以及將來的增長規(guī)模,對于短期就可能變?yōu)榇蠹系膋ey,要預(yù)估O(N)操作的元素數(shù)量,避免全量操作,可以使用HSCAN、SSCAN、ZSCAN進(jìn)行漸進(jìn)操作。集合元素數(shù)量過大在使用過程中會影響redis的實際性能,元素個數(shù)建議盡量不要超過5000,元素數(shù)量過大可考慮拆分成多個key進(jìn)行處理。 合理設(shè)置過期時間 某投票功能,用于統(tǒng)計今日環(huán)比昨日的增長數(shù)量,開發(fā)人員使用redis存儲每天的投票數(shù),key設(shè)計為vote_count_{date},其中{date}為當(dāng)天的日期,由于沒有設(shè)置過期時間,一年以后產(chǎn)生了360多個key,實際在用的key始終只有2個。 該案例中,每個生成的key在2天以后都不會再使用了,可將key加上過期時間。 某統(tǒng)計功能,用戶會不定期的導(dǎo)入一批數(shù)據(jù)進(jìn) redis,每一批數(shù)據(jù)需要在30分鐘后、1天后、3天后、7天后進(jìn)行計算統(tǒng)計,統(tǒng)計結(jié)果發(fā)給用戶。開發(fā)人員使用redis的同一個sortedset存儲這些導(dǎo)入的數(shù)據(jù),每天定時任務(wù)執(zhí)行計算任務(wù)。由于沒有清理,導(dǎo)致大量結(jié)束計算任務(wù)的廢棄數(shù)據(jù)殘留redis。 該案例中,每一批數(shù)據(jù)都有相應(yīng)的生命周期,在導(dǎo)入的第7天執(zhí)行完最后一次計算任務(wù)生命周期結(jié)束,由于集合里的元素不能單獨設(shè)置過期時間,可在代碼邏輯中對最后一次使用這批數(shù)據(jù)后進(jìn)行清理操作。 如果key沒有設(shè)置超時時間,會導(dǎo)致一直占用內(nèi)存。對于可以預(yù)估使用生命周期的key應(yīng)當(dāng)設(shè)置合理的過期時間或在最后一次操作時進(jìn)行清理,避免垃圾數(shù)據(jù)殘留redis。 合理利用批操作命令 某運營需求,需要給用戶生成短鏈,短鏈由短鏈前綴 短碼組成,根據(jù)短碼找到用戶對應(yīng)的手機(jī)號,開發(fā)人員使用redis hash結(jié)構(gòu)存儲短碼到手機(jī)號的映射。接口每次會導(dǎo)入5萬個手機(jī)號。 下面是開發(fā)人員的三種操作redis方案的偽代碼 方案1:直接使用redis的HSET逐個設(shè)置 for(50000;) 方案2:改用redis的 HMSET一次將所有元素設(shè)置到hash中 map<短碼,手機(jī)號> 50000個元素 方案3:依然使用 HMSET,只是每次設(shè)置500個,循環(huán)100次 map<短碼,手機(jī)號> 500個元素 對于大量頻繁的hset操作可以使用 HMSET替代減少redis操作次數(shù)同時提升處理速度,但是要考慮單次請求操作的數(shù)量,避免慢日志。 在redis使用過程中,要正視網(wǎng)絡(luò)往返時間,合理利用批量操作命令,減少通訊時延和redis訪問頻次。redis為了減少大量小數(shù)據(jù)CMD操作的網(wǎng)絡(luò)通訊時間開銷 RTT (Round Trip Time),支持多種批操作技術(shù):
減少不必要的請求 某業(yè)務(wù)系統(tǒng),當(dāng)用戶進(jìn)入某個頁面時會同時請求多個接口,每個接口都會校驗用戶狀態(tài)是否有效,用戶狀態(tài)存在redis里并設(shè)置有過期時間,對于key未過期但是過期時間大于指定閾值的,需要重新設(shè)置有效時間,否則需要使用del命令刪除掉。但是部分key由于過期其實已經(jīng)不存在了,所以出現(xiàn)部分無效del命令。用戶越多,就會有越多的無效命令。 ttl命令對于key不存在的情況會返回-2,若key不存在則不需要再調(diào)用del命令,可減少無效請求。 redis的所有請求對于不存在的key都會有輸出返回,合理利用返回值處理,避免不必要的請求,提升業(yè)務(wù)吞吐量。 避免value設(shè)置過大 某開發(fā)人員將一個商品集合信息序列化后用redis的字符串類型存儲,使用的時候再反序列化成對象列表使用,大小超過1MB,在網(wǎng)絡(luò)傳輸?shù)臅r候由于數(shù)據(jù)比較大會觸發(fā)拆包,會降低redis的吞吐量。 數(shù)量比較多時可以考慮改用hash結(jié)構(gòu)存儲,每一個field是商品id,value是該商品對象,如果數(shù)量較大可使用hscan獲取。 String類型盡量控制在10KB以內(nèi)。雖然redis對單個key可以緩存的對象長度能夠支持的很大,但是實際使用場合一定要合理拆分過大的緩存項,1k 基本是redis性能的一個拐點。當(dāng)緩存項超過10k、100k、1m性能下降會特別明顯。關(guān)于吞吐量與數(shù)據(jù)大小的關(guān)系可見下面官方網(wǎng)站提供的示意圖。 吞吐量與數(shù)據(jù)大小的關(guān)系 在局域網(wǎng)環(huán)境下只要傳輸?shù)陌怀^一個 MTU(以太網(wǎng)下大約 1500 bytes),那么對于 10、100、1000 bytes不同包大小的處理吞吐能力實際結(jié)果差不多。 設(shè)計規(guī)范的key名 以業(yè)務(wù)名為前綴,用冒號分隔,可使用業(yè)務(wù)名:子業(yè)務(wù)名:id的結(jié)構(gòu)命名,子業(yè)務(wù)下多單詞可再用下劃線分隔 舉例:活動系統(tǒng)-人拉人紅包活動-id,可命名為 ACTIVITY:INVITE_REDPACKET:001 保證語義的前提下,控制key的長度,當(dāng)key較多時,內(nèi)存占用也不容忽視 不包含空格、換行、單雙引號以及其他轉(zhuǎn)義字符 留心禁用命令 keys、monitor、flushall、flushdb應(yīng)當(dāng)通過redis的rename機(jī)制禁掉命令,若沒有禁用,開發(fā)人員要謹(jǐn)慎使用。其中flushall、flushdb會清空redis數(shù)據(jù);keys命令可能會引起慢日志;monitor命令在開啟的情況下會降低redis的吞吐量,根據(jù)壓測結(jié)果大概會降低redis50%的吞吐量,越多客戶端開啟該命令,吞吐量下降會越多。 keys和monitor在一些必要的情況下還是有助于排查線上問題的,建議可在重命名后在必要情況下由redis相關(guān)負(fù)責(zé)人員在redis備機(jī)使用,monitor命令可借助redis-faina等腳本工具進(jìn)行輔助分析,能更快排查線上ops飆升等問題。 本文整理出的幾點redis開發(fā)規(guī)范主要是涉及redis客戶端的使用部分,每個開發(fā)人員在使用redis開發(fā)過程中幾乎都會涉及到上述提到的幾個問題,需要多多留心,提高代碼質(zhì)量,提升redis的健康度。 作者:張家江 |
|