緩存的使用與設計前端
1.受益node
加速讀寫
CPU L1/L2/L3 Cache、瀏覽器緩存、Ehcache緩存數據庫結果
下降後端負載
後端服務器經過前端緩存下降負載:業務端使用Redis下降後端MySQL的負載redis
2.成本算法
數據不一致:緩存層和數據層有時間窗口不一致問題,和更新策略有關
代碼維護成本:多了一層緩存邏輯
運維成本:例如Redis Cluster數據庫
3.使用場景後端
下降後端負載
對高消耗的SQL:join結果集/分組統計結果緩存
加速請求響應
利用Redis/Memcache優化IO響應時間
大量寫合併爲批量寫
入計數器先Redis累加再批量寫DB瀏覽器
緩存的更新策略緩存
LRU等算法剔除:例如 maxmemory-policy服務器
2.超時剔除:例如expire網絡
3.主動更新:開發控制生命週期
4.兩條建議
低一致性數據:最大內存和淘汰策略
高一致性:超時剔除和主動更新結合,最大內存和淘汰策略兜底
緩存粒度問題
通用性:全量屬性更好
佔用空間:部分屬性更好
代碼維護:表面上全量屬性更好
緩存穿透優化
查詢一個不存在的數據,因爲緩存是不命中時須要從數據庫查詢,查不到數據則不寫入緩存,這將致使這個不存在的數據每次請求都要到數據庫去查詢
產生緣由
業務代碼自身問題
惡意攻擊、爬蟲
發現問題
業務的響應時間,受到惡意攻擊時,廣泛請求被打到存儲層,必會引發響應時間提升,可經過監控發現
業務自己問題
相關指標:總調用數、緩存層命中數、存儲層命中數
解決方法1:緩存空對象(設置過時時間)
當存儲層查詢不到數據後,往cache層中存儲一個null,後期再被查詢時,能夠經過cache返回null。
缺點
cache層須要存儲更多的key
緩存層和數據層數據「短時間」不一致
1 public String getPassThrough(String key) { 2 String cacheValue = cache.get(key); 3 if( StringUtils.isEmpty(cacheValue) ) { 4 String storageValue = storage.get(key); 5 cache.set(key , storageValue); 6 if( StringUtils.isEmpty(storageValue) ) { 7 cache.expire(key , 60 * 5 ); 8 } 9 return storageValue; 10 } else { 11 return cacheValue; 12 } 13 }
解決方法2:布隆過濾器攔截(適合固定的數據)
將全部可能存在的數據哈希到一個足夠大的bitmap中,一個不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力
緩存雪崩優化
因爲cache服務器承載大量的請求,當cache服務異常脫機,流量直接壓向後端組件,形成級聯故障。或者緩存集中在一段時間內失效,發生大量的緩存穿透
解決方法1:保證緩存高可用性
作到緩存多節點、多機器、甚至多機房。
Redis Cluster、Redis Sentinel
作二級緩存
解決方法2:依賴隔離組件爲後端限流
使用Hystrix作服務降級
解決方法3:提早演練(壓力測試)
解決方法4:對不一樣的key隨機設置過時時間
無底洞問題
添加機器時,客戶端的性能不但沒提高,反而降低
關鍵點
更多的機器 != 更高的性能
更多的機器 = 數據增加與水平擴展
批量接口需求:一次mget隨着機器增多,網絡節點訪問次數更多。網絡節點的時間複雜度由O(1) -> O(node)
優化IO的方法
命令自己優化:減小慢查詢命令:keys、hgetall、查詢bigKey並進行優化
減小網絡通訊次數
mget由O(keys),升級爲O(node),O(max_slow(node)) , 甚至是O(1)
下降接入成本:例如客戶端長鏈接/鏈接池、NIO等
熱點Key的重建優化
熱點Key(訪問量比較大) + 較長的重建時間(重建過程當中的API或者接口比較費時間)
致使的問題:有大量的線程會去查詢數據源並重建緩存,對存儲層形成了巨大的壓力,響應時間會變得很慢
1.三個目標
減小重建緩存的次數
數據儘量一致
減小潛在危險:例如死鎖、線程池大量被hang住(懸掛)
2.兩種解決方案
互斥鎖(分佈式鎖)
第一個線程須要重建時候,對這個Key的重建加入分佈式鎖,重建完成後進行解鎖
這個方法避免了大量的緩存重建與存儲層的壓力,可是仍是會有大量線程的阻塞
jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime) String get(String key) { String SET_IF_NOT_EXIST = "NX"; String SET_WITH_EXPIRE_TIME = "PX"; String value = jedis.get(key); if( null == value ) { String lockKey = "lockKey:" + key; if( "OK".equals(jedis.set(lockKey , "1" , SET_IF_NOT_EXIST , SET_WITH_EXPIRE_TIME , 180)) ) { value = db.get(key); jedis.set(key , value); jedis.delete(lockKey); } else { Thread.sleep(50); get(key); } } return value; }
永遠不過時
緩存層面:不設置過時時間(不使用expire)
功能層面:爲每一個value添加邏輯過時時間,單發現超過邏輯過時時間後,會使用單獨的線程去重建緩存。
還存在一個數據不一致的狀況。能夠將邏輯過時時間相對實際過時時間相對減少
Redis規模化的困擾
發佈構建繁瑣,私搭亂蓋
節點&機器等運維成本
監控報警比較低級
CacheCloud的主要功能
一鍵開啓Redis(單點、Sentinel、Cluster)
機器、應用、實例監控和報警
客戶端:透明使用、性能上報
可視化運維:配置、擴容、Failover、機器/應用/實例上下線
已存在Redis直接移入和數據遷移
布隆過濾器引出
問題引出:現有50億個電話號碼,有10萬個電話號碼,要快速準確判斷這些電話號碼是否已經存在
1. 經過數據庫查詢:實現快速有點難
2. 數據預先放在內存集合中:50億 * 8字節 = 40GB(***內存***浪費或不夠)
3. hyperloglog:準確有點難
布隆過濾器原理
須要參數
m個二進制向量(m個二進制位數)
n個預備數據(相似於問題中的50億個電話號碼)
k個hash函數(每一個數據,逐個進行hash,將指定向量標識爲1)
構建布隆過濾器
將n個數據逐個走一遍hash流程
判斷元素是否存在
將元素逐個hash進行執行
若是獲得的hash結果再向量中全都是1,則代表存在,反之當前元素不存在
布隆過濾器偏差率
對的數據的返回結果必然是對的,可是錯誤的數據也多是對的
直觀因素
m / n 的比例:比例越大,偏差率越小
hash函數的個數:個數越多,偏差率越小
本地布隆過濾器
實現類庫:Guava
1 Funnel<Integer> funnel = Funnels.integerFunnel(); 2 int size = 1000_000; 3 double errorChance = 0.001; //錯誤率 4 BloomFilter<Integer> filter = BloomFilter.create(funnel , size , errorChance); 5 for(int i = 0 ; i < size ; i++ ) { 6 filter.put(i); 7 } 8 for(int i = 0 ; i < size ; i++ ) { 9 if( !filter.mightContain(i) ) { 10 System.out.println("發現不存在的數據 : " + i); 11 } 12 }
布隆過濾器的問題
容量受到限制
多個應用存在多個布隆過濾器,構建同步過濾器複雜
Redis單機布隆過濾器
基於bitmap實現,利用setbit、getbit命令實現
hash函數:
MD5
murmur3_128
murmur3_32
sha1
sha256
sha512
基於Redis單機實現存在的問題
速度慢:與本機比,輸在網絡
解決方法1:單獨部署、與應用同機房甚至同機架部署
解決方法2:使用pipeline
容量限制:Redis最大字符串爲512MB、Redis單機容量
解決方法:基於RedisCluster實現
Redis Cluster實現布隆過濾器
多個布隆過濾器:二次路由
基於pipeline提升效率
內存管理
內存使用統計
info memory
2.內存消耗劃分
used_memory的組成部分
自身內存(800K左右)
緩衝內存
客戶端緩衝區
複製緩衝區
AOF緩衝區
對象內存
Key對象
Value對象
Lua內存
內存碎片 = used_memory_rss - used_memory(申請內存的會超過使用內存,內存預留與內存暫未釋放)
3.子進程內存消耗
redis在bgsave與bgrewriteaof時候都會使用fork建立出子進程,fork是copy-on-write的,當發生寫操做時,就會複製出一分內存
優化方式
去除THP特性:在kernel 2.6.38中新增的特性。這個特性能夠加快fork的速度,可是在複製內存頁的時候,會比原來的內存頁擴大了512倍。例如原來是4K,擴大後爲2M。當寫入量比較大時,會形成沒必要要的阻塞與內存的暴增。
overcommit_memory=1 能夠保證fork順利完成
#查看已鏈接客戶端的基本信息
info clients
#查看全部Redis-Server的全部客戶端的詳細信息
client list
輸入緩衝區
各個客戶端執行的Redis命令會存儲在Redis的輸入緩衝區中,由單線程排隊執行
- 注意:輸入緩衝區最大1GB,超事後會被強制斷開,不可動態配置
輸出緩衝區
1 client-output-buffer-limit ${class} ${hard limit} ${soft limit} ${soft seconds} 2 ## class 客戶端類型 3 ## normal 普通客戶端 4 ## slave 從節點用於複製,假裝成客戶端 5 ## pubsub 發佈訂閱客戶端 6 ## hardLimit 若是客戶端使用的輸出緩衝區大於hardLimit,客戶端會被當即關閉 7 ## softLimit 若是客戶端使用的輸出緩衝區超過了softLimit而且持續了softSeconds秒,客戶端會當即關閉 8 ## 其中hardLimit與softLimit爲0時不作任何限制
1.普通客戶端
默認配置:client-output-buffer-limit normal 0 0 0
注意:須要防止大命令或者monitor,這兩種狀況會致使輸出緩衝區暴增
2.slave客戶端
默認配置:client-output-buffer-limit slave 256mb 64mb 60
建議調大:可能主從延遲較高,或者從節點數量過多時,主從複製容易發生阻塞,緩衝區會快速打滿。最終致使全量複製
注意:在主從網絡中,從節點不要超過2個
3.pubsub客戶端
默認配置:client-output-buffer-limit pubsub 32mb 8mb 60
阻塞緣由:生產速度大於消費速度
注意:須要根據實際狀況進行調試
緩衝內存
1.複製緩衝區
此部份內存獨享,默認1MB,考慮部分複製,能夠設置更大,能夠設置爲100MB,犧牲部份內存避免全量複製。
2.AOF緩衝區
不管使用always、everysecs仍是no的策略,都會進行刷盤,刷盤前的數據存放在AOF緩衝區中。
另外在AOF重寫期間,會分配一塊AOF重寫緩衝區,重寫時的寫數據會存放在重寫緩衝區當中。
爲了不數據不一致的發生,在重寫完成後,會將AOF重寫緩衝區的內容,同步到AOF緩衝區中。
AOF相關的緩衝區沒有容量限制。
對象內存
Key:不要過長,量大不容忽視,建議控制在39字節以內。(embstr)
Value:儘可能控制其內部編碼爲壓縮編碼。
內存碎片
在jemalloc中必然存在內存碎片
緣由
與jemalloc有關,jemalloc會將內存空間劃分爲小、大、巨大三個範圍;每一個範圍又劃分了許多小的內存塊單位;存儲數據的時候,會選擇大小最合適的內存塊進行存儲。
redis做者爲了更好的性能,在redis中實現了本身的內存分配器來管理內存,不用的內存不會立刻返還給OS,從而實現提升性能。
修改cache的值,且修改後的value與原先的value大小差別較大。
優化方法
重啓redis服務,
redis4.0以上能夠設置自動清理 config set activedefrag yes,也能夠經過memory purge手動清理,配置監控使用性能最佳。
修改分配器(不推薦,須要對各個分配器十分了解)
避免頻繁更新操做
內存回收策略
1.刪除過時鍵值
惰性刪除
1. 訪問key
2. expired dict
3. del key
定時刪除:每秒運行10次,採樣刪除
2.maxmemory-policy
Noevition:默認策略,不會刪除任何數據,拒絕全部寫入操做並返回錯誤信息。
volatile-lru:根據lru算法刪除設置了超時屬性的key,直到騰出空間爲止。若是沒有可刪除的key,則將退回到noevition。
allkeys-lru:根據lru算法刪除key,無論數據有沒有設置超時屬性,直到騰出足夠空間爲止。
allkeys-random:隨機刪除全部鍵,直到騰出足夠空間爲止。
volatile-random:隨機刪除過時鍵,直到騰出足夠空間爲止。
volatile-ttl:根據鍵值對象的ttl屬性,刪除最近將要過時的數據。若是沒有,回退到noevicition。
客戶端緩衝區優化
案例:一次線上事故,主從節點配置的maxmemory都是4GB,發現主節點的使用內存達到了4GB即內存已打滿。而從節點只有2GB。
思考方向
考慮Redis的內存自身組成
主從節點以前數據傳輸不一致(會致使對象內存不一致)
dbsize
查看info replication中的slave_repl_offset
查看緩衝區方面
經過info clients查看最大的緩衝區佔用
經過client list查看全部客戶端的詳細信息
問題發現:
存在一個monitor客戶端對命令進行監聽,因爲Redis的QPS極高,monitor客戶端沒法及時處理,佔用緩衝區。
問題預防
運維層面:線上禁用monitor(rename monitor "")
運維層面:適度限制緩衝區大小
開發層面:理解monitor原理,能夠短暫尋找熱點key
開發層面:使用CacheCloud能夠直接監控到相關信息
內存優化其餘建議
不要忽視key的長度:1個億的key,每一個字節都是節省
序列化和壓縮方法:拒絕Java原生序列化,能夠採用Protobuf、kryo等
不建議使用Redis的場景
數據:大且冷的數據
功能性:關係型查詢、消息隊列
完