天天看點

緩存的一些問題和一些加密算法【緩存問題】

緩存

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' 
                   
  • 一個視圖的響應結果
    • 考慮單一的視圖 ,隻隻對特定的視圖結果進行緩存
    @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  指明記憶體淘汰政策
           

總結:

  1. 如果将redis作為持久存儲 ,記憶體淘汰政策 采用預設配置 noeviction
  2. 如果将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

    1号使用者關注過 2 3 4 5 6 7
               
    每個人單獨一條redis記錄
    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

多思考也是一種努力,做出正确的分析和選擇,因為我們的時間和精力都有限,是以把時間花在更有價值的地方。