Redis 緩存

緩存類型

客戶端緩存
對於BS架構的互聯網應用來說客戶端緩存主要分爲頁面緩存和瀏覽器緩存兩種,對於APP而言主要是自身所使用的緩存。

清理緩存的方式:
以網頁瀏覽器爲例
1.使用Ctrl+F5可以強制刷新瀏覽器本地緩存
2.瀏覽器中的參數設置的時間已過期如:Expires,Cache-control: max-age, Last-Modified標籤等

網絡中緩存
網絡中的緩存主要是指代理服務器對客戶端請求數據的緩存,主要分爲WEB代理緩存和邊緣緩存(CDN邊緣緩存)

反向代理緩存

基本介紹
反向代理位於應用服務器機房,處理所有對WEB服務器的請求。

如果用戶請求的頁面在代理服務器上有緩衝的話,代理服務器直接將緩衝內容發送給用戶。如果沒有緩衝則先向WEB服務器發出請求,取回數據,本地緩存後再發送給用戶。通過降低向WEB服務器的請求數,從而降低了WEB服務器的負載。

應用場景

一般只緩存體積較小靜態文件資源,如css、js、圖片

開源實現

主要爲nginx

服務端緩存
1.堆緩存

堆緩存:使用Java堆內存來存儲緩存對象,位於服務本機的內存中

優點:沒有序列化/反序列化,省去網絡傳輸過程是最快的緩存;
缺點:
1)緩存數據量有限,受限於堆空間大小,一般存軟應用/弱引用對象即當堆空間不足時GC會強制回收釋放空間,數據同步較麻煩;
2)單服務是集羣部署的時候,應該考慮是否需要做集羣中本地緩存的數據同步
常用堆緩存Guava 、Map等

2.堆外緩存

堆外緩存:緩存對象存儲在堆外,應用中的緩存組件,應用和cache是在同一個進程內部,請求緩存非常快速,沒有過多的網絡開銷;
優點:更大容量的緩存空間,減少GC掃描和移動對象;
缺點:讀取數據時需要序列化,速度比堆內緩存慢,數據同步較麻煩;
常用堆緩存Ehcahe等

EhCache
特點:
EhCache是進程內的緩存框架,在集羣時不推薦使用
EhCache在第一次通過select查詢出的結果被加入到EhCache緩存中,第二次查詢從EhCache取出的對象與第一次查詢對象實際上是同一個對象,因此我們在更新age的時候,實際已經更新了EhCache中的緩存對象。
故EhCache自帶刷新緩存能力

3.分佈式緩存

分佈式緩存:分佈式緩存由一個服務端實現管理和控制,有一個或多個客戶端節點存儲數據的緩存;
優點:具有較大容量的緩存空間,易於擴容;相對於堆(外)緩存、磁盤緩存更容易保證數據一致性;
缺點:需要序列化/反序列化及網絡傳輸

常用的分佈式緩存 Redis Memcached

4.數據庫緩存
數據庫在設計的時候也有緩存操作,更改相關參數開啓查詢緩存

CDN

什麼是CDN

CDN的全稱是Content Delivery Network,即內容分發網絡。CDN是構建在網絡之上的內容分發網絡,依靠部署在各地的邊緣服務器,通過中心平臺的負載均衡、內容分發、調度等功能模塊,使用戶就近獲取所需內容,降低網絡擁塞,提高用戶訪問響應速度和命中率。CDN的關鍵技術主要有內容存儲和分發技術。

CDN的優勢:
(1)CDN節點解決了跨運營商和跨地域訪問的問題,訪問延時大大降低;
(2)大部分請求在CDN邊緣節點完成,CDN起到了分流作用,減輕了源站的負載。

引入CDN後的網路架構如圖:

在這裏插入圖片描述

客戶端瀏覽器先檢查是否有本地緩存是否過期,如果過期,則向CDN邊緣節點發起請求,CDN邊緣節點會檢測用戶請求數據的緩存是否過期,如果沒有過期,則直接響應用戶請求,此時一個完成http請求結束;如果數據已經過期,那麼CDN還需要向源站發出回源請求(back to the source request),來拉取最新的數據。CDN的典型拓撲圖如下:

在這裏插入圖片描述

CDN緩存

瀏覽器本地緩存失效後,瀏覽器會向CDN邊緣節點發起請求。類似瀏覽器緩存,CDN邊緣節點也存在着一套緩存機制。

CDN緩存的缺點

CDN的分流作用不僅減少了用戶的訪問延時,也減少的源站的負載。但其缺點也很明顯:當網站更新時,如果CDN節點上數據沒有及時更新,即便用戶再瀏覽器使用Ctrl +F5的方式使瀏覽器端的緩存失效,也會因爲CDN邊緣節點沒有同步最新數據而導致用戶訪問異常。

CDN緩存策略

CDN邊緣節點緩存策略因服務商不同而不同,但一般都會遵循http標準協議,通過http響應頭中的Cache-control: max-age的字段來設置CDN邊緣節點數據緩存時間。

當客戶端向CDN節點請求數據時,CDN節點會判斷緩存數據是否過期,若緩存數據並沒有過期,則直接將緩存數據返回給客戶端;否則,CDN節點就會向源站發出回源請求,從源站拉取最新數據,更新本地緩存,並將最新數據返回給客戶端。

CDN服務商一般會提供基於文件後綴、目錄多個維度來指定CDN緩存時間,爲用戶提供更精細化的緩存管理。

CDN緩存時間會對“回源率”產生直接的影響。若CDN緩存時間較短,CDN邊緣節點上的數據會經常失效,導致頻繁回源,增加了源站的負載,同時也增大的訪問延時;若CDN緩存時間太長,會帶來數據更新時間慢的問題。開發者需要增對特定的業務,來做特定的數據緩存時間管理。

CDN緩存刷新

CDN邊緣節點對開發者是透明的,相比於瀏覽器Ctrl+F5的強制刷新來使瀏覽器本地緩存失效,開發者可以通過CDN服務商提供的“刷新緩存”接口來達到清理CDN邊緣節點緩存的目的。這樣開發者在更新數據後,可以使用“刷新緩存”功能來強制CDN節點上的數據緩存過期,保證客戶端在訪問時,拉取到最新的數據。

更多CDN相關技術請參考:
1.CDN學習筆記一(CDN是什麼?)https://www.cnblogs.com/tinywan/p/6067126.html

一、Redis

Redis 簡介
Redis本質上是一個Key-Value類型的內存數據庫,它將整個數據庫統統加載在內存當中進行操作,定期通過異步操作把數據庫數據flush到硬盤上進行保存。因爲是純內存操作,Redis的性能非常出色,每秒可以處理超過 10萬次讀寫操作.
Redis 與其他 key - value 緩存產品有以下三個特點:

1.Redis支持數據的持久化,可以將內存中的數據保持在磁盤中,重啓的時候可以再次從磁盤中加載數據進行使用。可以對部分數據加入內存中,下次訪問則無需訪問數據庫,直接訪問內存即可,極大的加快了訪問速度,對於熱點商品,併發量大的極有意義

持久化的過程(具體方式待續)

Redis爲了達到最快的讀寫速度將數據都讀到內存中,並通過異步的方式將數據寫入磁盤。

2.Redis不僅僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
3.Redis支持數據的備份,即master-slave模式的數據備份

缺點:
1.Redis的主要缺點是數據庫容量受到物理內存的限制,不能用作海量數據的高性能讀寫,因此Redis適合的場景主要侷限在較小數據量的高性能操作和運算上。
2.耗內存,當redis內存達到一定容量時,讀寫性能會下降

查看redis內存使用情況命令(待續)

redis使用場景
1.做緩存:
冷熱隔離:即熱點數據和併發量不大的數據
2.分佈式session
3.分佈式鎖
4.隊列
Reids在內存存儲引擎領域的一大優點是提供 list 和 set 結構,這使得Redis能作爲一個很好的消息隊列平臺來使用。
5.發佈/訂閱????
分佈式環境下爲何使用redis?

主要解決的是性能和併發,還可以使用redis實現分佈式鎖。

可參考:爲什麼分佈式一定要有redis? https://www.cnblogs.com/bigben0123/p/9115597.html

二。Redis 數據類型
1.String(字符串)
string是redis最基本的類型,你可以理解成與Memcached一模一樣的類型,一個key對應一個value,但一個字符串類型的值能存儲最大容量是512M。string類型是二進制安全的。意思是redis的string可以包含任何數據。比如jpg圖片或者序列化的對象 。一般做一些複雜的計數功能的緩存。
實例

redis 127.0.0.1:6379> SET name "w3cschool.cn"
OK
redis 127.0.0.1:6379> GET name
"w3cschool.cn"

2.Set(集合)
Set 就是一個集合,集合的概念就是一堆不重複值的組合。利用 Redis 提供的 Set 數據結構,可以存儲一些集合性的數據。

如:

redis 127.0.0.1:6379> sadd w3cschool.cn rabitmq
(integer) 1
redis 127.0.0.1:6379> sadd w3cschool.cn rabitmq
(integer) 0

Redis DEL 命令用於刪除已存在的鍵。不存在的 key 會被忽略。
如果鍵被刪除成功,命令執行後輸出 (integer) 1,否則將輸出 (integer) 0

Redis Smembers 命令返回集合中的所有的成員。 不存在的集合 key 被視爲空集合。如:

redis 127.0.0.1:6379> SADD myset1 "hello"
(integer) 1
redis 127.0.0.1:6379> SADD myset1 "world"
(integer) 1
redis 127.0.0.1:6379> SMEMBERS myset1
1) "World"
2) "Hello"

3.sorted set有序集合)
sorted set多了一個權重參數score,集合中的元素能夠按score進行排列。可以做排行榜應用,取TOP N操作。sorted set可以用來做延時任務。最後一個應用就是可以做範圍查找。

4.列表(List)
List數據結構是鏈表結構,是雙向的,可以在鏈表左,右兩邊分別操作;
使用List的數據結構,可以做簡單的消息隊列的功能(既可以保證消息的順序性)。另外還有一個就是,可以利用lrange命令,做基於redis的分頁功能,性能極佳,用戶體驗好。

5.哈希(Hash)
這裏value存放的是結構化的對象,比較方便的就是操作其中的某個字段。如在做單點登錄的時候,就是用這種數據結構存儲用戶信息,以cookieId作爲key,設置30分鐘爲緩存過期時間,能很好的模擬出類似session的效果。

Redis 事務

Redis 事務可以一次執行多個命令, 並且帶有以下兩個重要的保證:
事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。
事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。
一個事務從開始到執行會經歷以下三個階段:
開始事務。
命令入隊。
執行事務。

redis 線程模型

分析:這個問題其實是對redis內部機制的一個考察。很多人其實都不知道redis是單線程工作模型。所以,這個問題還是應該要複習一下的。

回答:主要是以下三點
(一)純內存操作
(二)單線程操作,避免了頻繁的上下文切換
(三)採用了非阻塞I/O多路複用機制

redis線程模型,如圖所示

在這裏插入圖片描述

簡單來說,就是。我們的redis-client在操作的時候,會產生具有不同事件類型的socket。在服務端,有一段I/0多路複用程序,將其置入隊列之中。然後,文件事件分派器,依次去隊列中取,轉發到不同的事件處理器中。
需要說明的是,這個I/O多路複用機制,redis還提供了select、epoll、evport、kqueue等多路複用函數庫,大家可以自行去了解。

I/O多路複用機制請參考: https://www.cnblogs.com/dolphin0520/p/3916526.html

redis架構演進

1.Redis 單機版
在這裏插入圖片描述

單機版三個問題:
1、 內存容量有限
2、處理能力有限
3、無法高可用。
2.Redis 多機版

主要有三種方案:1、複製( Replication) 2、哨兵(Sentinel) 3、集羣(Cluster)。
Redis 多機版特性功能:
複製:擴展系統對於讀的能力
哨兵:爲服務器提供高可用特性,減少故障停機出現
集羣:擴展內存容量,增加機器,提高性能讀寫能力和存儲以及提供高可用特性

1.主從Redis

Redis 的複製( replication)功能允許用戶根據一個 Redis 服務器來創建任意多個該服務器的複製品,其中被複制的服務器爲主服務器( master),而通過複製創建出來的服務器複製品則爲從服務器( slave)。 只要主從服務器之間的網絡連接正常,主從服務器兩者會具有相同的數據,主服務器就會一直將發生在自己身上的數據更新同步 給從服務器,從而一直保證主從服務器的數據相同
其架構圖如下:
在這裏插入圖片描述

優點:

降低 master 讀壓力,講讀壓力轉交給了從庫

缺點:

 1.人爲關注Master是否宕機

 2.無法完成自動切換主從

 3.沒有解決 master 寫的壓力

2.哨兵
當主數據庫遇到異常中斷服務後,開發者可以通過手動的方式選擇一個從數據庫來升格爲主數據庫,以使得系統能夠繼續提供服務。然而整個過程相對麻煩且需要人工介入,難以實現自動化。 爲此,Redis 2.8開始提供了哨兵工具來實現自動化的系統監控和故障恢復功能。 哨兵的作用就是監控redis主、從數據庫是否正常運行,主出現故障自動將從數據庫轉換爲主數據庫。

顧名思義,哨兵的作用就是監控Redis系統的運行狀況。它的功能包括以下兩個。
(1)監控主數據庫和從數據庫是否正常運行。
(2)主數據庫出現故障時自動將從數據庫轉換爲主數據庫。
其模型如圖:

在這裏插入圖片描述

Master與slave的切換過程:
(1)slave leader升級爲master
(2)其他slave修改爲新master的slave
(3)客戶端修改連接
(4)老的master如果重啓成功,變爲新master的slave

集羣

redis-cluster集羣

即使使用哨兵,redis每個實例也是全量存儲,每個redis存儲的內容都是完整的數據,浪費內存且有木桶效應。爲了最大化利用內存,可以採用cluster羣集,就是分佈式存儲。即每臺redis存儲不同的內容。

採用redis-cluster架構正是滿足這種分佈式存儲要求的集羣的一種體現。redis-cluster架構中,被設計成共有16384個hash slot。每個master分得一部分slot,其算法爲:hash_slot = crc16(key) mod 16384 ,這就找到對應slot。採用hash slot的算法,實際上是解決了redis-cluster架構下,有多個master節點的時候,數據如何分佈到這些節點上去。key是可用key,如果有{}則取{}內的作爲可用key,否則整個可以是可用key。羣集至少需要3主3從,且每個實例使用不同的配置文件。

其架構如圖所示:

在這裏插入圖片描述

redis-cluster集羣的高可用

在redis-cluster架構中,redis-master節點一般用於接收讀寫,而redis-slave節點則一般只用於備份,其與對應的master擁有相同的slot集合,若某個redis-master意外失效,則再將其對應的slave進行升級爲臨時redis-master。

節點分區

數據分區方案

1.哈希取餘分區

哈希取餘分區思路非常簡單:計算key的hash值,然後對節點數量進行取餘,從而決定數據映射到哪個節點上。

該方案最大的問題是,當新增或刪減節點時,節點數量發生變化,系統中所有的數據都需要重新計算映射關係,引發大規模數據遷移。

2.一致性哈希分區

一致性哈希算法將整個哈希值空間組織成一個虛擬的圓環,如下圖所示,範圍爲0-2^32-1;對於每個數據,根據key計算hash值,確定數據在環上的位置,然後從此位置沿環順時針行走,找到的第一臺服務器就是其應該映射到的服務器

在這裏插入圖片描述

優點:與哈希取餘分區相比,一致性哈希分區將增減節點的影響限制在相鄰節點。以上圖爲例,如果在node1和node2之間增加node5,則只有node2中的一部分數據會遷移到node5;如果去掉node2,則原node2中的數據只會遷移到node4中,只有node4會受影響。

缺點:一致性哈希分區的主要問題在於,當節點數量較少時,增加或刪減節點,對單個節點的影響可能很大,造成數據的嚴重不平衡。還是以上圖爲例,如果去掉node2,node4中的數據由總數據的1/4左右變爲1/2左右,與其他節點相比負載過高。

3.帶虛擬節點的一致性哈希分區
該方案在一致性哈希分區的基礎上,引入了虛擬節點的概念。Redis集羣使用的便是該方案,其中的虛擬節點稱爲槽(slot)。槽是介於數據和實際節點之間的虛擬概念;每個實際節點包含一定數量的槽,每個槽包含哈希值在一定範圍內的數據。引入槽以後,數據的映射關係由數據hash->實際節點,變成了數據hash->槽->實際節點。

在使用了槽的一致性哈希分區中,槽是數據管理和遷移的基本單位。槽解耦了數據和實際節點之間的關係,增加或刪除節點對系統的影響很小,實際只會對槽進行重新分配。仍以上圖爲例,系統中有4個實際節點,假設爲其分配16個槽(0-15); 槽0-3位於node1,4-7位於node2,以此類推。如果此時刪除node2,只需要將槽4-7重新分配即可,例如槽4-5分配給node1,槽6分配給node3,槽7分配給node4;可以看出刪除node2後,數據在其他節點的分佈仍然較爲均衡。

槽的數量一般遠小於2^32,遠大於實際節點的數量;在Redis集羣中,槽的數量爲16384。

下面這張圖很好的總結了Redis集羣將數據映射到實際節點的過程:

在這裏插入圖片描述

(1)Redis對數據的特徵值(一般是key)計算哈希值,使用的算法是CRC16。

(2)根據哈希值,計算數據屬於哪個槽。使用算法:哈希值對16384取模

(3)根據槽與節點的映射關係,計算數據屬於哪個節點。

分區的原理是採用帶虛擬節點的一致性哈希分區

一致性哈希相關的原理請參考: 程序員小灰-漫畫:什麼是一致性哈希?https://blog.csdn.net/xaccpJ2EE/article/details/82993120?utm_source=blogxgwz7

客戶端訪問集羣
edis-cli

(1)計算key屬於哪個槽:CRC16(key) & 16383

(2)判斷key所在的槽是否在當前節點:假設key位於第i個槽,clusterState.slots[i]則指向了槽所在的節點,如果clusterState.slots[i]==clusterState.myself,說明槽在當前節點,可以直接在當前節點執行命令;
否則,說明槽不在當前節點,則查詢槽所在節點的地址(clusterState.slots[i].ip/port),並將其包裝到MOVED錯誤中返回給redis-cli。

(3)redis-cli收到MOVED錯誤後,根據返回的ip和port重新發送請求。

具體例子:請參考:深入學習Redis(5):集羣https://www.cnblogs.com/kismetv/p/9853040.html

緩存使用

緩存命中率:
緩存命中率是從緩存中讀取數據的次數與總讀取次數的比率,命中率越高越好。
緩存命中率 = 從緩存中讀取次數 / {總讀取次數 (從緩存中讀取次數 +從慢設備上讀取次數) }
這個指標非常重要,直接影響到系統性能,也可以通過該指標來衡量緩存是否運行良好;

緩存帶來的問題:

緩存穿透
即黑客故意去請求緩存中不存在的數據,導致所有的請求都懟到數據庫上,從而數據庫連接異常。

解決方案:
(一)利用互斥鎖,緩存失效的時候,先去獲得鎖,得到鎖了,再去請求數據庫。沒得到鎖,則休眠一段時間重試。其實質就是限制流量,類似的方法還有使用消息隊列進行流量消峯。
(二)採用異步更新策略,無論key是否取到值,都直接返回。這樣就不用請求數據庫了。value值中維護一個緩存失效時間,緩存如果過期,異步起一個線程去讀數據庫,更新緩存。需要做緩存預熱(項目啓動前,先加載緩存)操作。
但這樣對用戶來說獲取數據就直接變成了從緩存獲取,適合訪問頻率級高的.
(三)提供一個能迅速判斷請求是否有效的攔截機制,比如,利用布隆過濾器,內部維護一系列合法有效的key。迅速判斷出,請求所攜帶的Key是否合法有效。如果不合法,則直接返回。合法的話才進入緩存判斷。

緩存雪崩
即緩存同一時間大面積的失效,這個時候又來了一波請求,結果請求都懟到數據庫上,從而導致數據庫連接異常。

解決辦法:
(一)給緩存的失效時間,加上一個隨機值,避免集體失效。即在設置過期時間時,讓所配置的過期時間在乘以一個隨機值
(二)使用互斥鎖,但是該方案吞吐量明顯下降了。即流量消峯
(三)雙緩存。我們有兩個緩存,緩存A和緩存B。緩存A的失效時間爲20分鐘,緩存B不設失效時間。自己做緩存預熱操作。然後細分以下幾個小點

1)從緩存A讀數據庫,有則直接返回

2)A沒有數據,直接從B讀數據,直接返回,並且異步啓動一個更新線程。
3)更新線程同時更新緩存A和緩存B。

但這樣會增大內存空間。緩存B可以使用lru算法來兼容空間問題。

緩存擊穿
指的是併發量很高的 KEY,在該 KEY 失效的瞬間有很多請求同同時去請求數據庫,更新緩存。例如我們有一個業務 KEY,該 KEY 的併發請求量爲 10000。當該 KEY 失效的時候,就會有 1 萬個線程會去請求數據庫更新緩存。這個時候如果沒有采取適當的措施,那麼數據庫很可能崩潰。

解決辦法:

1.使用互斥鎖

當從緩存獲取的是null值時,其他key 處於等待現象,必須等待第一個構建完緩存之後,釋放鎖,其他人才能通過該key才能訪問數據;那其他人就直接從緩存裏面取數據,不會造成數據庫的讀寫性能的缺陷;

原理如下圖:

在這裏插入圖片描述

如果是單機,可以用synchronized或者lock來處理,如果是分佈式環境可以用分佈式鎖就可以了(分佈式鎖,可以用memcache的add, redis的setnx, zookeeper的添加節點操作)。

分佈式版源碼如下:

public String get(key) {
      String value = redis.get(key);
      if (value == null) { //代表緩存值過期
        // 3 min timeout to avoid mutex holder crash
		  if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表設置成功
               value = db.get(key);
                      redis.set(key, value, expire_secs);
                      redis.del(key_mutex);
              } else {  //這個時候代表同時候的其他線程已經load db並回設到緩存了,這時候重試獲取緩存值即可
                      sleep(50);
                      get(key);  //重試
              }
          } else {
              return value;      
          }
 }

  1. "提前"使用互斥鎖(mutex key):

在value內部設置1個超時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上延長timeout1並重新設置到cache。然後再從數據庫加載數據並設置到cache中。

  1. “永遠不過期”:

這裏的“永遠不過期”包含兩層意思:

  • 從redis上看,確實沒有設置過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期。
  • 從功能上看,如果不過期,那不就成靜態的了嗎?所以我們把過期時間存在key對應的value裏,如果發現要過期了,通過一個後臺的異步線程進行緩存的構建,也就是“邏輯”不過期

其實現原理如下:

在這裏插入圖片描述

源碼如下:

String get(final String key) {  
        V v = redis.get(key);  
        String value = v.getValue();  
        long timeout = v.getTimeout();  
        if (v.timeout <= System.currentTimeMillis()) {  
            // 異步更新後臺異常執行  
            threadPool.execute(new Runnable() {  
                public void run() {  
                    String keyMutex = "mutex:" + key;  
                    if (redis.setnx(keyMutex, "1")) {  
                        // 3 min timeout to avoid mutex holder crash  
                        redis.expire(keyMutex, 3 * 60);  
                        String dbValue = db.get(key);  
                        redis.set(key, dbValue);  
                        redis.delete(keyMutex);  
                    }  
                }  
            });  
        }  
        return value;  
    }  

從實戰看,這種方法對於性能非常友好,唯一不足的就是構建緩存時候,其餘線程(非構建緩存的線程)可能訪問的是老數據,但是對於一般的互聯網功能來說這個還是可以忍受。

  1. 資源保護:

採用netflix的hystrix,可以做資源的隔離保護主線程池,如果把這個應用到緩存的構建也未嘗不可。

四種解決方案:沒有最佳只有最合適

在這裏插入圖片描述

如何解決redis的併發競爭key問題

分析:這個問題大致就是,同時有多個子系統去set一個key。可能會導致的問題:數據庫和緩存的數據不一致。

回答:如下所示
(1)如果對這個key操作,不要求順序
這種情況下,準備一個分佈式鎖,大家去搶鎖,搶到鎖就做set操作即可,比較簡單。
(2)如果對這個key操作,要求順序
假設有一個key1,系統A需要將key1設置爲valueA,系統B需要將key1設置爲valueB,系統C需要將key1設置爲valueC.
期望按照key1的value值按照 valueA–>valueB–>valueC的順序變化。這種時候我們在數據寫入數據庫的時候,需要保存一個時間戳。假設時間戳如下

系統A key 1 {valueA 3:00}
系統B key 1 {valueB 3:05}
系統C key 1 {valueC 3:10}

那麼,假設這會系統B先搶到鎖,將key1設置爲{valueB 3:05}。接下來系統A搶到鎖,發現自己的valueA的時間戳早於緩存中的時間戳,那就不做set操作了。以此類推。

其他方法,比如利用隊列,將set方法變成串行訪問也可以。總之,靈活變通

Redis相比memcached有哪些優勢?

(1) memcached所有的值均是簡單的字符串,redis作爲其替代者,支持更爲豐富的數據類型
(2) redis可以持久化其數據

redis的過期策略以及內存淘汰機制

分析:這個問題其實相當重要,到底redis有沒用到家,這個問題就可以看出來。比如你redis只能存5G數據,可是你寫了10G,那會刪5G的數據。怎麼刪的,這個問題思考過麼?還有,你的數據已經設置了過期時間,但是時間到了,內存佔用率還是比較高,有思考過原因麼?

回答:
redis採用的是定期刪除+惰性刪除策略。

redis過期策略
爲什麼不用定時刪除策略?
定時刪除,用一個定時器來負責監視key,過期則自動刪除。雖然內存及時釋放,但是十分消耗CPU資源。在大併發請求下,CPU要將時間應用在處理請求,而不是刪除key,因此沒有采用這一策略.

定期刪除+惰性刪除是如何工作的呢?
定期刪除:redis默認每個100ms檢查,是否有過期的key,有過期key則刪除。需要說明的是,redis不是每個100ms將所有的key檢查一次,而是隨機抽取進行檢查(如果每隔100ms,全部key進行檢查,redis豈不是卡死)。因此,如果只採用定期刪除策略,會導致很多key到時間沒有刪除。

惰性刪除:於是,惰性刪除派上用場。也就是說在你獲取某個key的時候,redis會檢查一下,這個key如果設置了過期時間那麼是否過期了?如果過期了此時就會刪除。

採用定期刪除+惰性刪除就沒其他問題了麼?
不是的,如果定期刪除沒刪除key。然後你也沒即時去請求key,也就是說惰性刪除也沒生效。這樣,redis的內存會越來越高。那麼就應該採用內存淘汰機制。

內存淘汰機制
在redis.conf中有一行配置

maxmemory-policy volatile-lru

該配置就是配內存淘汰策略的(什麼,你沒配過?好好反省一下自己),redis默認的策略是volatile-lru

1)noeviction:當內存不足以容納新寫入數據時,新寫入操作會報錯。應該沒人用吧。
2)allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key。推薦使用,目前項目在用這種。
3)allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個key。應該也沒人用吧,你不刪最少使用Key,去隨機刪。
4)volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key。這種情況一般是把redis既當緩存,又做持久化存儲的時候才用。不推薦
5)volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個key。依然不推薦
6)volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的key優先移除。不推薦
ps:如果沒有設置 expire 的key, 不滿足先決條件(prerequisites); 那麼 volatile-lru, volatile-random 和 volatile-ttl 策略的行爲, 和 noeviction(不刪除) 基本上一致。

redis和數據庫雙寫一致性問題

分析:一致性問題是分佈式常見問題,還可以再分爲最終一致性和強一致性。數據庫和緩存雙寫,就必然會存在不一致的問題。答這個問題,先明白一個前提。就是如果對數據有強一致性要求,不能放緩存。我們所做的一切,只能保證最終一致性。另外,我們所做的方案其實從根本上來說,只能說降低不一致發生的概率,無法完全避免。因此,有強一致性要求的數據,不能放緩存。

回答:首先,採取正確更新策略,先更新數據庫,再刪緩存。其次,因爲可能存在刪除緩存失敗的問題,提供一個補償措施即可,例如利用消息隊列的重試機制

分析:

將不一致分爲三種情況:

  1. 數據庫有數據,緩存沒有數據;

  2. 數據庫有數據,緩存也有數據,數據不相等;

  3. 數據庫沒有數據,緩存有數據。

讀請求:

不要求強一致性的讀請求,走redis,要求強一致性的直接從mysql讀取。讀操作優先讀取redis,不存在的話就去訪問MySQL,並把讀到的數據寫回Redis中;

寫請求:

數據首先都寫到數據庫,然後把緩存裏對應的數據失效掉(刪掉)。(先寫redis再寫mysql,如果寫入失敗事務回滾會造成redis中存在髒數據)

思考:爲何是刪緩存而不是更新緩存?

這麼做引發的問題是,如果A,B兩個線程同時做數據更新,A先更新了數據庫,B後更新數據庫,則此時數據庫裏存的是B的數據。
1)而更新緩存的時候,是B先更新了緩存,而A後更新了緩存,則緩存裏是A的數據。這樣緩存和數據庫的數據也不一致。
2)A更新成功了,B更新失敗了。則緩存裏是A的數據。這樣緩存和數據庫的數據也不一致。

刪除緩存失敗的補償方案參考:

  1. 對刪除緩存進行重試,數據的一致性要求越高,我越是重試得快。(個人理解當刪除緩存失敗後,應該還要爲當前key值設置標記,當下一次對當前key值有查詢請求時,直接讀數據庫,直到刪除失敗的標記消失爲止)

  2. 定期全量更新,簡單地說,就是我定期把緩存全部清掉,然後再全量加載。

  3. 給所有的緩存一個失效期。

第三種方案可以說是一個大殺器,任何不一致,都可以靠失效期解決,失效期越短,數據一致性越高。但是失效期越短,查數據庫就會越頻繁。因此失效期應該根據業務來定。

併發情況:

併發不高的情況:

讀: 讀redis->沒有,讀mysql->把mysql數據寫回redis,有的話直接從redis中取;

寫: 寫mysql->成功,再寫redis;

併發高的情況:

讀: 讀redis->沒有,讀mysql->把mysql數據寫回redis,有的話直接從redis中取;

寫:異步話,先寫入redis的緩存,就直接返回;定期或特定動作將數據保存到mysql,可以做到多次更新,一次保存;

場景分析:

1.在高併發的情況下,如果當刪除完緩存的時候,這時去更新數據庫,但還沒有更新完,另外一個請求來查詢數據,發現緩存裏沒有,就去數據庫裏查,
還是以上面商品庫存爲例,如果數據庫中產品的庫存是100,那麼查詢到的庫存是100,然後插入緩存,插入完緩存後,此時緩存的值爲100.原來那個更新數據庫的線程把數據庫更新爲了99,導致數據庫與緩存不一致的情況

參考方案:

遇到這種情況,可以用隊列的去解決這個問,創建幾個隊列,如20個,當有數據更新請求時,先把它丟到隊列裏去,當更新完後在從隊列裏去除,如果在更新的過程中,遇到以上場景,先去緩存裏看下有沒有數據,如果沒有,可以先去隊列裏看是否有相同商品ID在做更新,如果有也把查詢的請求發送到隊列裏去,然後同步等待緩存更新完成。
這裏有一個優化點,如果發現隊列裏有一個查詢請求了,那麼就不要放新的查詢操作進去了,用一個while(true)循環去查詢緩存,循環個200MS左右,如果緩存裏還沒有則直接取數據庫的舊數據(不要更新緩存),一般情況下是可以取到的。

參考:
1.Redis怎麼保持緩存與數據庫一致性? https://blog.csdn.net/belalds/article/details/82078009
2.Redis 如何保持和MySQL數據一致 https://blog.csdn.net/thousa_ho/article/details/78900563

多級緩存

所謂多級緩存,即在整個系統架構的不同系統層級進行數據緩存,以提升訪問效率,這也是應用最廣的方案之一。我們應用的整體架構如圖所示:

在這裏插入圖片描述

整體流程如上圖所示:

1)首先接入Nginx將請求負載均衡到應用Nginx,此處常用的負載均衡算法是輪詢或者一致性哈希,輪詢可以使服務器的請求更加均衡,而一致性哈希可以提升應用Nginx的緩存命中率,相對於輪詢,一致性哈希會存在單機熱點問題,一種解決辦法是熱點直接推送到接入層Nginx,一種辦法是設置一個閥值,當超過閥值,改爲輪詢算法。

2)接着應用Nginx讀取本地緩存(本地緩存可以使用Lua Shared Dict、Nginx Proxy Cache(磁盤/內存)、Local Redis實現),如果本地緩存命中則直接返回,使用應用Nginx本地緩存可以提升整體的吞吐量,降低後端的壓力,尤其應對熱點問題非常有效。

3)如果Nginx本地緩存沒命中,則會讀取相應的分佈式緩存(如Redis緩存,另外可以考慮使用主從架構來提升性能和吞吐量),如果分佈式緩存命中則直接返回相應數據(並回寫到Nginx本地緩存)。

4)如果分佈式緩存也沒有命中,則會回源到Tomcat集羣,在回源到Tomcat集羣時也可以使用輪詢和一致性哈希作爲負載均衡算法。

5)在Tomcat應用中,首先讀取本地堆緩存,如果有則直接返回(並會寫到主Redis集羣),爲什麼要加一層本地堆緩存將在緩存崩潰與快速修復部分細聊。

6)作爲可選部分,如果步驟4沒有命中可以再嘗試一次讀主Redis集羣操作,。目的是防止當從有問題時的流量衝擊。

7)如果所有緩存都沒有命中只能查詢DB或相關服務獲取相關數據並返回。

8)步驟7返回的數據異步寫到主Redis集羣,此處可能多個Tomcat實例同時寫主Redis集羣,可能造成數據錯亂,如何解決該問題將在更新緩存與原子性部分細聊。

應用整體分了三部分緩存:應用Nginx本地緩存、分佈式緩存、Tomcat堆緩存,每一層緩存都用來解決相關的問題,如應用Nginx本地緩存用來解決熱點緩存問題,分佈式緩存用來減少訪問回源率、Tomcat堆緩存用於防止相關緩存失效/崩潰之後的衝擊。

參考資料:
1.爲什麼分佈式一定要有redis? https://www.cnblogs.com/bigben0123/p/9115597.html
2.redis 五種數據結構詳解(string,list,set,zset,hash)https://www.cnblogs.com/sdgf/p/6244937.html
3.redis架構演變與redis-cluster羣集讀寫方案 https://yq.aliyun.com/articles/625912?utm_content=m_1000013190
4.程序員小灰-漫畫:什麼是一致性哈希?https://blog.csdn.net/xaccpJ2EE/article/details/82993120?utm_source=blogxgwz7
5.深入學習Redis(5):集羣https://www.cnblogs.com/kismetv/p/9853040.html

6.緩存穿透,緩存擊穿,緩存雪崩解決方案分析https://blog.csdn.net/zeb_perfect/article/details/54135506

7.深入理解分佈式系統中的緩存架構(上) https://blog.csdn.net/yelvgou9995/article/details/81079463

8.《深入分佈式緩存》之“億級請求下多級緩存那些事 https://blog.csdn.net/wireless_com/article/details/79134166

發佈了67 篇原創文章 · 獲贊 22 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章