緩存架構(gòu)
腦中的直觀反應(yīng)
計算機(jī)體系結(jié)構(gòu)中的緩存
多級緩存
- SQLAlchemy起到一定的本地緩存作用
在同一請求中多次相同的查詢只查詢數(shù)據(jù)庫一次缰趋,SQLAlchemy做了本地緩存(類似Django中的Queryset查詢結(jié)果集)- 使用Redis構(gòu)建一層緩存
緩存數(shù)據(jù)
緩存數(shù)據(jù)的類型
- 一個數(shù)值
例如
- 驗證碼
- 用戶狀態(tài)
如:user:{user_id}: enable- 數(shù)據(jù)庫記錄
- Caching at the object level
以數(shù)據(jù)庫對象的角度考慮踪央, 應(yīng)用更普遍
例如, 用戶的基本信息user = User.query.filter_by(id=1).first() user -> User對象 { 'user_id':1, 'user_name': 'python', 'age': 28, 'introduction': '' }
- Caching at the database query level
以數(shù)據(jù)庫查詢的角度考慮丰包,應(yīng)用場景較特殊顶霞,一般僅針對較復(fù)雜的查詢進(jìn)行使用query_result = User.query.join(User.profile).filter_by(id=1).first() -> sql = "select a.user_id, a.user_name, b.gender, b.birthday from tbl_user as a inner join tbl_profile as b on a.user_id=b.user_id where a.user_id=1;" # hash算法 md5 query = md5(sql) # 'fwoifhwoiehfiowy23982f92h929y3209hf209fh2' # redis setex(query, expiry, json.dumps(query_result))
- 一個視圖的響應(yīng)結(jié)果
@route('/articles') @cache(exipry=30*60) def get_articles(): ch = request.args.get('ch') articles = Article.query.all() for article in articles: user = User.query.filter_by(id=article.user_id).first() comment = Comment.query.filter_by(article_id=article.id).all() results = {...} # 格式化輸出 return results # redis # '/artciels?ch=1': json.dumps(results)
- 一個頁面
@route('/articles') @cache(exipry=30*60) def get_articles(): ch = request.args.get('ch') articles = Article.query.all() for article in articles: user = User.query.filter_by(id=article.user_id).first() comment = Comment.query.all() results = {...} return render_template('article_temp', results) # redis # '/artciels?ch=1': html
緩存數(shù)據(jù)的保存方式
- 序列化字符串
如# 序列化 json字符 # setex('user:{user_id}:info') setex('user:1:info', expiry, json.dumps(user_dict))
- 優(yōu)點(diǎn)
- 存儲字符串節(jié)省空間
- 缺點(diǎn)
- 序列化有時間開銷
- 更新不方便(一般直接刪除)
- 優(yōu)點(diǎn)
- Redis的其他數(shù)據(jù)類型,如hash光绕、set、zset
如hmset('user:1:info', user_dict)
- 優(yōu)點(diǎn)
- 讀寫時不需要序列化轉(zhuǎn)換
- 可以更新內(nèi)部數(shù)據(jù)
- 缺點(diǎn)
- 相比字符串畜份,采用復(fù)合結(jié)構(gòu)存儲空間占用大
- 優(yōu)點(diǎn)
緩存有效期與淘汰策略
有效期 TTL (Time to live)
設(shè)置有效期的作用:
1.節(jié)省空間
2.做到數(shù)據(jù)弱一致性诞帐,有效期失效后,可以保證數(shù)據(jù)的一致性
Redis的過期策略
過期策略通常有以下三種:
- 定時過期
每個設(shè)置過期時間的key都需要創(chuàng)建一個定時器爆雹,到過期時間就會立即清除停蕉。該策略可以立即清除過期的數(shù)據(jù),對內(nèi)存很友好钙态;但是會占用大量的CPU資源去處理過期的數(shù)據(jù)慧起,從而影響緩存的響應(yīng)時間和吞吐量。setex('a', 300, 'aval') setex('b', 600, 'bval')
- 惰性過期
只有當(dāng)訪問一個key時驯绎,才會判斷該key是否已過期完慧,過期則清除谋旦。該策略可以最大化地節(jié)省CPU資源剩失,卻對內(nèi)存非常不友好。極端情況可能出現(xiàn)大量的過期key沒有再次被訪問册着,從而不會被清除拴孤,占用大量內(nèi)存。- 定期過期
每隔一定的時間甲捏,會掃描一定數(shù)量的數(shù)據(jù)庫的expires字典中一定數(shù)量的key演熟,并清除其中已過期的key。該策略是前兩者的一個折中方案。通過調(diào)整定時掃描的時間間隔和每次掃描的限定耗時芒粹,可以在不同情況下使得CPU和內(nèi)存資源達(dá)到最優(yōu)的平衡效果兄纺。expires字典會保存所有設(shè)置了過期時間的key的過期時間數(shù)據(jù),其中化漆,key是指向鍵空間中的某個鍵的指針估脆,value是該鍵的毫秒精度的UNIX時間戳表示的過期時間。鍵空間是指該Redis集群中保存的所有鍵座云。
Redis中同時使用了惰性過期和定期過期兩種過期策略疙赠。
Redis過期刪除采用的是定期刪除,默認(rèn)是每100ms檢測一次朦拖,遇到過期的key則進(jìn)行刪除圃阳,這里的檢測并不是順序檢測,而是隨機(jī)檢測璧帝。那這樣會不會有漏網(wǎng)之魚捍岳?顯然Redis也考慮到了這一點(diǎn),當(dāng)我們?nèi)プx/寫一個已經(jīng)過期的key時睬隶,會觸發(fā)Redis的惰性刪除策略祟同,直接回干掉過期的key
為什么不用定時刪除策略?
定時刪除,用一個定時器來負(fù)責(zé)監(jiān)視key,過期則自動刪除。雖然內(nèi)存及時釋放理疙,但是十分消耗CPU資源晕城。在大并發(fā)請求下,CPU要將時間應(yīng)用在處理請求窖贤,而不是刪除key,因此沒有采用這一策略.
定期刪除+惰性刪除是如何工作的呢?
定期刪除砖顷,redis默認(rèn)每個100ms檢查,是否有過期的key,有過期key則刪除赃梧。需要說明的是滤蝠,redis不是每個100ms將所有的key檢查一次,而是隨機(jī)抽取進(jìn)行檢查(如果每隔100ms,全部key進(jìn)行檢查授嘀,redis豈不是卡死)物咳。因此,如果只采用定期刪除策略蹄皱,會導(dǎo)致很多key到時間沒有刪除览闰。
于是,惰性刪除派上用場巷折。也就是說在你獲取某個key的時候压鉴,redis會檢查一下,這個key如果設(shè)置了過期時間那么是否過期了锻拘?如果過期了此時就會刪除油吭。
采用定期刪除+惰性刪除就沒其他問題了么?
不是的击蹲,如果定期刪除沒刪除key。然后你也沒即時去請求key婉宰,也就是說惰性刪除也沒生效歌豺。這樣,redis的內(nèi)存會越來越高心包。那么就應(yīng)該采用內(nèi)存淘汰機(jī)制世曾。
緩存淘汰 eviction
Redis自身實現(xiàn)了緩存淘汰
Redis的內(nèi)存淘汰策略是指在Redis的用于緩存的內(nèi)存不足時,怎么處理需要新寫入且需要申請額外空間的數(shù)據(jù)谴咸。
- noeviction:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時轮听,新寫入操作會報錯。
- allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時岭佳,在鍵空間中血巍,移除最近最少使用的key。
- allkeys-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時珊随,在鍵空間中述寡,隨機(jī)移除某個key。
- volatile-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時叶洞,在設(shè)置了過期時間的鍵空間中鲫凶,移除最近最少使用的key。
- volatile-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時衩辟,在設(shè)置了過期時間的鍵空間中螟炫,隨機(jī)移除某個key。
- volatile-ttl:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時艺晴,在設(shè)置了過期時間的鍵空間中昼钻,有更早過期時間的key優(yōu)先移除。
redis 4.x 后支持LFU策略封寞,最少頻率使用
- allkeys-lfu
- volatile-lfu
LRU
LRU(Least recently used然评,最近最少使用)
LRU算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進(jìn)行淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過狈究,那么將來被訪問的幾率也更高”碗淌。
基本思路
1.新數(shù)據(jù)插入到列表頭部;
2.每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問)抖锥,則將數(shù)據(jù)移到列表頭部亿眠;
3.當(dāng)列表滿的時候,將列表尾部的數(shù)據(jù)丟棄宁改。
LFU
LFU(Least Frequently Used 最近最少使用算法)
它是基于“如果一個數(shù)據(jù)在最近一段時間內(nèi)使用次數(shù)很少缕探,那么在將來一段時間內(nèi)被使用的可能性也很小”的思路。
LFU需要定期衰減还蹲。
Redis淘汰策略的配置
- maxmemory 最大使用內(nèi)存數(shù)量
- maxmemory-policy noeviction 淘汰策略
緩存模式
1.Cache Aside
更新方式
-
先更新數(shù)據(jù)庫,再更新緩存。這種做法最大的問題就是兩個并發(fā)的寫操作導(dǎo)致臟數(shù)據(jù)谜喊。如下圖(以Redis和Mysql為例)潭兽,兩個并發(fā)更新操作,數(shù)據(jù)庫先更新的反而后更新緩存斗遏,數(shù)據(jù)庫后更新的反而先更新緩存山卦。這樣就會造成數(shù)據(jù)庫和緩存中的數(shù)據(jù)不一致,應(yīng)用程序中讀取的都是臟數(shù)據(jù)诵次。
-
先刪除緩存账蓉,再更新數(shù)據(jù)庫。這個邏輯是錯誤的逾一,因為兩個并發(fā)的讀和寫操作導(dǎo)致臟數(shù)據(jù)铸本。如下圖(以Redis和Mysql為例)。假設(shè)更新操作先刪除了緩存遵堵,此時正好有一個并發(fā)的讀操作箱玷,沒有命中緩存后從數(shù)據(jù)庫中取出老數(shù)據(jù)并且更新回緩存,這個時候更新操作也完成了數(shù)據(jù)庫更新陌宿。此時锡足,數(shù)據(jù)庫和緩存中的數(shù)據(jù)不一致,應(yīng)用程序中讀取的都是原來的數(shù)據(jù)(臟數(shù)據(jù))壳坪。
-
先更新數(shù)據(jù)庫舶得,再刪除緩存。這種做法其實不能算是坑爽蝴,在實際的系統(tǒng)中也推薦使用這種方式扩灯。但是這種方式理論上還是可能存在問題。如下圖(以Redis和Mysql為例)霜瘪,查詢操作沒有命中緩存珠插,然后查詢出數(shù)據(jù)庫的老數(shù)據(jù)。此時有一個并發(fā)的更新操作颖对,更新操作在讀操作之后更新了數(shù)據(jù)庫中的數(shù)據(jù)并且刪除了緩存中的數(shù)據(jù)捻撑。然而讀操作將從數(shù)據(jù)庫中讀取出的老數(shù)據(jù)更新回了緩存。這樣就會造成數(shù)據(jù)庫和緩存中的數(shù)據(jù)不一致缤底,應(yīng)用程序中讀取的都是原來的數(shù)據(jù)(臟數(shù)據(jù))顾患。
但是,仔細(xì)想一想个唧,這種并發(fā)的概率極低江解。因為這個條件需要發(fā)生在讀緩存時緩存失效,而且有一個并發(fā)的寫操作徙歼。實際上數(shù)據(jù)庫的寫操作會比讀操作慢得多犁河,而且還要加鎖鳖枕,而讀操作必需在寫操作前進(jìn)入數(shù)據(jù)庫操作,又要晚于寫操作更新緩存桨螺,所有這些條件都具備的概率并不大宾符。但是為了避免這種極端情況造成臟數(shù)據(jù)所產(chǎn)生的影響,我們還是要為緩存設(shè)置過期時間灭翔。
2.Read-through 通讀
3.Write-through 通寫
4.Write-behind caching
示例方案
- 使用Read-throught + Cache aside
- 構(gòu)建一層抽象出來的緩存操作層魏烫,負(fù)責(zé)數(shù)據(jù)庫查詢和Redis緩存存取,在Flask的視圖邏輯中直接操作緩存層工具肝箱。
- 更新采用先更新數(shù)據(jù)庫哄褒,再刪除緩存
緩存問題
1緩存穿透
緩存只是為了緩解數(shù)據(jù)庫壓力而添加的一層保護(hù)層,當(dāng)從緩存中查詢不到我們需要的數(shù)據(jù)就要去數(shù)據(jù)庫中查詢了煌张。如果被黑客利用呐赡,頻繁去訪問緩存中沒有的數(shù)據(jù),那么緩存就失去了存在的意義唱矛,瞬間所有請求的壓力都落在了數(shù)據(jù)庫上罚舱,這樣會導(dǎo)致數(shù)據(jù)庫連接異常。
解決方案:
1.約定:對于返回為NULL的依然緩存绎谦,對于拋出異常的返回不進(jìn)行緩存,注意不要把拋異常的也給緩存了管闷。采用這種手段的會增加我們緩存的維護(hù)成本,需要在插入緩存的時候刪除這個空緩存窃肠,當(dāng)然我們可以通過設(shè)置較短的超時時間來解決這個問題包个。
2.制定一些規(guī)則過濾一些不可能存在的數(shù)據(jù),小數(shù)據(jù)用BitMap冤留,大數(shù)據(jù)可以用布隆過濾器碧囊,比如你的訂單ID 明顯是在一個范圍1-1000,如果不是1-1000之內(nèi)的數(shù)據(jù)那其實可以直接給過濾掉纤怒。
緩存雪崩
緩存雪崩是指緩存不可用或者大量緩存由于超時時間相同在同一時間段失效糯而,大量請求直接訪問數(shù)據(jù)庫,數(shù)據(jù)庫壓力過大導(dǎo)致系統(tǒng)雪崩泊窘。
解決方案:
1熄驼、給緩存加上一定區(qū)間內(nèi)的隨機(jī)生效時間,不同的key設(shè)置不同的失效時間烘豹,避免同一時間集體失效瓜贾。比如以前是設(shè)置10分鐘的超時時間,那每個Key都可以隨機(jī)8-13分鐘過期携悯,盡量讓不同Key的過期時間不同祭芦。
2、采用多級緩存憔鬼,不同級別緩存設(shè)置的超時時間不同龟劲,及時某個級別緩存都過期胃夏,也有其他級別緩存兜底。
3咸灿、利用加鎖或者隊列方式避免過多請求同時對服務(wù)器進(jìn)行讀寫操作构订。