緩存
1 需求背景
- 緩存不是必須的,是為了提升性能而增加的
- 目标: 減少磁盤資料庫的查詢,比如mysql的查詢 ,更多的從記憶體中讀取資料
- mysql查詢 通常在1s左右 (幾百毫秒, 0.xxs),1s以上通常認為是慢查詢
- redis 支援操作的性能 1s可以支援1w+ 操作(更高 可達10W+)
- 場景
- 前提: 讀取頻繁
- 資料不經常變化,基本一定會做緩存處理
- 資料可能變化頻繁,如果資料是産品的核心資料(比如評論資料),可以考慮建構緩存, 緩存時間短,即時緩存5分鐘,也能減少很大程度的資料庫查詢操作,可以提升性能
- 前提: 讀取頻繁
2 緩存架構
資料存在哪?
多級緩存
- 本地緩存
- 全局變量儲存
- orm架構 queryset 查詢集(查詢結果集) 起到本地緩存的作用
- django orm
- sqlalchemy
- 外部緩存
- 可以建構多級
- 外部存儲
- redis
- memcached
3 緩存資料
儲存哪些資料 ? 資料以什麼形式(類型)儲存?
3.1 緩存的資料内容
- 一個數值
- 手機短信驗證碼
- 比如使用者的狀态資料 user:status -> 0 / 1
- 資料庫記錄
- 不以單一視圖單獨考慮,而是考慮很多視圖可能都會用到一些公共資料,就把這些公共的資料緩存,哪個視圖用到,哪個視圖自己讀取緩存取資料 ,(比如使用者的個人資訊,文章的資訊)
- 比較通用,緩存一個資料可以被多個視圖利用,節省空間
- 方式:
- Caching at the object level 緩存資料對象級别
- 通用
mysql 中有使用者的個人資訊表 每條記錄 是一個使用者的資料 一個資料實體 user:1 -> user_id ,name mobile profile_photo intro certi user:20 -> user_id ,name mobile profile_photo intro certi
- Caching at the database query level 緩存資料庫查詢級别
- 相比緩存資料對象級别 不太通用,隻适用于比較複雜的查詢,才考慮使用
sql = 'select * from ..inner join where ... group by order by limit' -> query_results hash(sql) -> 'wicwiugfiwuegfwiugiw238' md5(sql) 緩存 資料名稱 資料内容 'wicwiugfiwuegfwiugiw238' -> query_results 使用的時候 sql -> md5(sql) -> 'wicwiugfiwuegfwiugiw238'
- Caching at the object level 緩存資料對象級别
- 一個視圖的響應結果
- 考慮單一的視圖 ,隻隻對特定的視圖結果進行緩存
@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 # /articles?ch=123 視圖的結果resuls 緩存 # 下一次再通路 ‘/articles?ch=123’
- 一個頁面
- 隻針對 h5頁面 (html5) 網頁
- 方式
- 如果是伺服器端渲染 (前後端不分離)
@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
- 頁面靜态化 算是一種頁面緩存方式
- 如果是伺服器端渲染 (前後端不分離)
3.2 緩存資料儲存形式
針對的是外部緩存 redis
- 字元串形式
user:1 -> user_id ,name mobile profile_photo intro certi user1 -> User()對象 -> user1_dict key value user:1 -> json.dumps(user1_dict) pickle.dumps() json: 1. 隻能接受 清單 字典 bytes類型 2. json轉換成字元串 效率速度慢 pickle : 1. 基本支援python中的所有類型,(包括自定義的類的對象) 2. json轉換成字元串 效率速度 快
- 優點: 儲存一組資料的時候,存儲占用的空間 相比其他類型可能節省空間
- 缺點:整存整取 ,如果想擷取其中的單一字段 不是很友善,需要整體取出 再序列化或反序列化, 更新某個字段 類似 , 不靈活
- 非字元串形式
- list set hash zset
- 需要針對特定的資料來選型
user:1 -> user_id ,name mobile profile_photo intro certi user1 -> User()對象 -> user1_dict key value user:1 -> hash { name: xxx, moible: xxx photo: xxx }
- 優點: 可以針對特定的字段進行讀寫,相對靈活
- 缺點: 儲存一組資料的時候,占用的空間相比字元串會稍大
4 緩存資料的有效期 TTL (time to live)
緩存資料一定要設定有效期,原因/作用:
- 即時清理可以節省空間
- 保證資料的一緻性,(弱一緻性) ,保證mysql中的資料與redis中的資料,在更新資料之後還能一直, 雖然在一定的時間内(緩存資料的有效期) redis與mysql中的資料不同 ,但是過了有效期後 redis會清理資料, 再次查詢資料時 會形成新的緩存資料,redis與mysql又相同了
4.1 redis的有效期政策
通用的有效期政策:
- 定時過期
set a 100 有效期 10min set b 100 有效期 20min
開啟一個計時器計時,當有效期到達之後 清理資料, 可以了解為每個資料都要單獨維護一個計時器
缺點: 耗費性能
-
惰性過期
儲存資料 設定有效期後 不主動維護這個資料的有效期,不計時,隻有在再次通路這個資料(讀寫)的時候,判斷資料是否到期,如果到期清理并傳回空,如果沒到期,傳回資料
- 定期過期
- 周期性的檢查哪些資料過期哪些資料沒過期,比如每100ms判斷哪些資料過期,如果有過期的資料,進行清理
Redis的有效期政策 : 惰性過期 + 定期過期
- redis實作定期過期的時候,還不是查詢所有資料,而是每100ms 随機選出一些資料判斷是否過期,再過100ms 再随機選出一些判斷
思考:
如果在redis中儲存了一條資料,設定有效期為10min,但是資料設定之後 再無操作, 請問 10min之後 這條資料是否還在redis的記憶體中? 答案: 還可能存在
5 緩存淘汰 (記憶體淘汰)
背景: redis的資料有效期政策不能保證資料真正的即時被清理,可能造成空間浪費,再有新的資料的時候,沒地方可以存存儲, 為了存儲新資料,需要清理redis中的一批資料,騰出空間儲存新資料
淘汰政策 指 删除哪些資料
通用的記憶體淘汰算法: LRU & LFU
-
LRU(Least recently used,最近最少使用)
思想: 認為 越是最近用過的資料,接下來使用的機會越大,應該清理那些很久以前使用過的資料
cache_data = [ cache1 時間最近 cache2 cache5 cache4 cache3 時間最遠 ] 操作過cache3 cache_data = [ cache3 cache1 時間最近 cache2 cache5 cache4 ] 增加cache6 cache_data = [ cache6 cache3 cache1 時間最近 cache2 cache5 ]
-
LFU (Least Frequently Used, 最少使用) 以頻率 次數來考慮
思想: 認為使用次數越多的資料,接下來使用的機會越大,應該清理那些使用次數少的資料
cache_data = { cache1 : 100 cache2: 2 cache5: 23 cache4: 89 cache3 : 10000 } 操作了cache2 cache_data = { cache1 : 100 cache2: 3 cache5: 23 cache4: 89 cache3 : 10000 } 新增 cache6 cache_data = { cache1 : 100 cache5: 23 cache4: 89 cache3 : 10000 cache6: 1 } cache_data = { cache1 : 100 -> 50 cache5: 23 -> 11 cache4: 89 -> 45 cache3 : 10000 -> 5000 cache6: 1 -> 1 }
- 效果更好
- 缺點: 性能不高,需要額外記錄次數 頻率, 還需要定期衰減
Redis的記憶體淘汰政策 (3.x版本以後)
- noeviction:當記憶體不足以容納新寫入資料時,新寫入操作會報錯。 預設
- allkeys-lru:當記憶體不足以容納新寫入資料時,在鍵空間中,移除最近最少使用的key。
- allkeys-random:當記憶體不足以容納新寫入資料時,在鍵空間中,随機移除某個key。
- volatile-lru:當記憶體不足以容納新寫入資料時,在設定了過期時間的鍵空間中,移除最近最少使用的key。
- volatile-random:當記憶體不足以容納新寫入資料時,在設定了過期時間的鍵空間中,随機移除某個key。
- volatile-ttl:當記憶體不足以容納新寫入資料時,在設定了過期時間的鍵空間中,有更早過期時間的key優先移除。
redis 4.x 版本之後 增加了兩種
- allkeys-lfu
- volatile-lfu
redis中的配置
maxmemory <bytes> 指明redis使用的最大記憶體上限
maxmemory-policy volatile-lru 指明記憶體淘汰政策
總結:
- 如果将redis作為持久存儲 ,記憶體淘汰政策 采用預設配置 noeviction
- 如果将redis作為緩存,需要配置記憶體淘汰政策 選擇合适的淘汰政策
6. 緩存模式
應用程式如何使用緩存
- 讀緩存
- 場景: 需要頻繁讀取查詢資料 的場景
- 方式 在應用程式與mysql資料庫中間架設redis 作為緩存 ,讀取資料的時候先從緩存中讀取, 但是寫入新資料的時候,直接儲存到mysql中
- 寫緩存
- 場景: 需要頻繁的儲存資料 的場景
- 方式 在應用程式與mysql資料庫中間架設redis 作為緩存 ,儲存資料的時候先儲存到緩存redis中,并不直接儲存的mysql中, 後續再從redis中同步資料到mysql中
讀緩存的資料同步問題:
修改了mysq中的資料,如何處理redis緩存中的資料
- 先更新資料庫,再更新緩存
- 先删除緩存,再更新資料庫
- 先更新資料庫,再删除緩存 發生問題的幾率最小 ,負面影響最小
7 緩存使用過程中可能存在的問題
- 緩存穿透
- 問題: 通路不存在的資料, 資料庫沒有 緩存也沒有存儲,每次通路都落到資料查詢
- 解決:
- 緩存中儲存不存在的資料,比如将資料以-1儲存,表示資料不存在,可以攔截 這種攻擊,減少資料庫查詢
- 需要引入其他工具 ,過濾器 ,按照規則來判斷 是否可能存在, 比如 布隆過濾器
- 緩存雪崩
- 問題: 同一批産生 的緩存資料 可能在同一時間失效,如果在同一時間大量的緩存失效,查詢時又會落到資料庫中查詢,對資料庫并發的有大量的查詢,資料庫吃不消,資料庫又可能崩潰
-
- 将資料的有效期增加偏內插補點,讓同一批産生的緩存資料不在同一時間 失效,将失效時間錯開
- 架設多級緩存, 每級緩存有效期不同
- 以保護資料庫出發, 為資料庫的操作 添加鎖 或者 放到隊列中,強行的将并行的資料庫操作改為串行操作,一個一個執行,防止資料庫崩潰
8 頭條項目的緩存的設計
- 伺服器硬體層面的架設
-
- orm 查詢結果集緩存
- 一級外部緩存
- redis cluster 配置了緩存淘汰政策 (無需配置 持久化政策) volatile-lru 4.0.13
-
- 程式編寫開發上
- 緩存 的資料 Caching at the object level 資料庫對象級别,可以被多個視圖利用
- 緩存資料一定要設定有效期 , 為了防止緩存雪崩,有效期要設定偏內插補點
- 為了防止緩存穿透,緩存資料時 不存在的資料也要緩存下來
- 大多數情況選擇 先更新資料庫 再删除緩存
9 頭條項目緩存的資料儲存形式
redis資料類型的設計 (redis資料類型選型)
redis 的list set hash zset 資料是不允許嵌套的, 資料元素都是字元串
- 使用者個人資訊資料 (類似參考的 文章緩存 評論緩存)
user1 -> User1 -> name mobile photo user2 -> User2 ->
- 設計方式1 所有使用者在redis中以一條記錄儲存
key value users:info -> str X json.dumps({'user1': cache_data, 'user2': cache_data}) list set X [json.dumps(user1_dict), json.dumps(user2_dict)] hash { 'user1': json.dumps(user1_dict), 'user2': json.dumps(user2_dict) } zset X member 成員 score 分數/權重 json.dumps(user1_dict) user_id
考慮有效期:
redis中的有效期不能對一條記錄中的不同字段單獨設定,最小隻能給一條記錄設定有效期
所有人隻能有一個有效期,不好 緩存雪崩
不采用
- 方式2 每個使用者在redis中單獨一條記錄
user1 -> User1 -> name mobile photo user2 -> User2 -> key value user:{user_id}:info user:1:info user:2:info -> str json.dumps(user2_dict) hash { "name": xxx, "mobile": xx 'photo':xxx } str: 占用空間小 頭條項目 為了儲存更多的緩存資料 選擇字元串 hash: 存取靈活
-
使用者關注清單資訊資料 ( 類似的還有 使用者的文章清單 文章的評論清單 使用者的粉絲清單等)
需要緩存的是關注裡中 關注的使用者的user_id
每個人單獨一條redis記錄1号使用者關注過 2 3 4 5 6 7
key value user:{user_id}:follows user:1:follows user:2:follows -> str json.dumps([2,3,4,5..user_id]) list set X ['2','3','4', 'use_id',..] hash X field value user_id_2 follow_time user_id_3 follow_time zset 有序集合 既能去重 還有序 member score user_id_2 follow_time user_id_3 follow_time 時間戳 str 使用者如果關注的人過多,整取資料不友善,而且清單一般是要分頁取 zset 可以批量分頁取資料 還能排序 頭條項目選擇zset 更新資料庫後 添加資料
10 頭條項目redis持久儲存的資料儲存形式
-
- redis 單機存儲容量足夠 ,再建構複制集 做高可用,防止主機redis挂掉
- 配置持久化存儲政策 RDB + AOF
- 記憶體淘汰政策 配置 noeviction
- 儲存的資料
- 閱讀曆史 搜尋曆史
- 統計資料 (之前使用資料庫反範式設計的 備援字段)比如使用者的關注數量 粉絲數量等
-
閱讀曆史 (文章id清單)
方式一: 所有人一條記錄 X
方式二: 每人一條記錄key value users:read:history str json.dumps({'user_1': [], user_2:[]}) list set X hash { "user_1": '2,3,4,5', "user_2": '100, 20, 30' } zset member score article_id user_id ‘2,3,4,5' user_id1 '100, 20, 30' user_id2
key value user:{user_id}:read:history user:1:read:history user:2:read:history -> list [artilce_id, 2, 3, 4, ...] set 沒有順序 X (artilce_id, 2, 3, 4, ...) hash X article_id read_time 2 16724383275342 3 163232763827822 zset 選擇 member score article_id read_time 2 16724383275342 3 163232763827822
-
統計資料
方式一
key value user:{user_id}:statistic user:1:statistic user:2:statistic -> hash { 'article_count': 120, "follow_count": xx, "fans_count": xxx, .. }
方式二: 采用
考慮營運平台可能需要對産品進行全平台大排名,比如 篩選釋出文章數量最多的前20名使用者 top問題
每個統計名額 一條redis記錄(儲存所有使用者這個統計名額的資料)
key value statistic:user:follows statistic:user:fans statistic:user:articles -> zset mebmer score user_id article_count 1 100 2 3 3 11
- list set zset hash 一條記錄能儲存的元素數量上限 42億
加密算法
- 散列 hash (比如密碼的處理)
- 特點:
- 不同的資料 計算之後得到的結果一定不同
- 相同的資料計算之後得到的結果相同
- 不可逆
- md5
- sha1
- sha256
- 特點:
- 簽名 (比如jwt token)
- HS256 簽名與驗簽時 使用相同的秘鑰字元串 進行sha256計算 -> 簽名值
- RS256 簽名與驗簽時 使用不同的秘鑰字元串 進行sha256計算 -> 簽名值
- 加密 (可以解密的)
- 對稱加密
- 加密與解密使用相同的秘鑰
- AES
- DES
- 非對稱加密
- 加密 與解密使用不同的秘鑰 (公鑰私鑰)
- RSA
- 對稱加密
多思考也是一種努力,做出正确的分析和選擇,因為我們的時間和精力都有限,是以把時間花在更有價值的地方。