Redis分散式快取剖析及大廠面試精髓v6.2.6
概述
**本人部落格網站 **IT小神 www.itxiaoshen.com
官方說明
Redis官網 https://redis.io/ 最新版本6.2.6
Redis中文官網 http://www.redis.cn/ 不過中文官網的同步更新維護相對要滯後不少時間,但對於我們基礎學習完成足夠了
Redis是一個開源(BSD許可)的記憶體資料結構儲存,用作資料庫、快取和訊息代理。Redis提供豐富的資料結構,如字串、雜湊、列表、集合、帶範圍查詢、點陣圖、超對數、地理空間索引和流的排序集。Redis具有內建的複製、Lua指令碼、LRU驅逐、事務和不同級別的磁碟永續性,並通過Redis Sentinel和Redis Cluster的自動分割槽提供高可用性。
Redis使用場景有哪些?
計數器、分散式ID生成器、海量資料統計bitmap、會話快取、分散式阻塞佇列、分散式鎖、熱點資料、社交需求好友推薦、延遲佇列(sortset)等。
Redis與Mysql的部分場景比較
- 高效能讀寫訪問,解決mysql讀寫慢的問題、緩解mysql壓力
- 具有較豐富可描述性資料結構和可擴充套件性。
- Redis有更高優勢應對訪問熱度問題,儲存熱點資料。
安裝
單機原始碼安裝
#Redis單機原始碼安裝非常簡單的,先下載,提取和編譯就可以拉起來使用,Redis單機一般用於開發和學習環境,生產使用的話一般都是使用Redis Sentinel或者Redis Cluster保證高可用性 wget https://download.redis.io/releases/redis-6.2.6.tar.gz tar xzf redis-6.2.6.tar.gz cd redis-6.2.6 make && make install
#在當前目錄下有redis的配置檔案redis.conf,先修改redis.conf中的daemonize值為yes讓redis以後臺程式方式執行
redis-server redis.conf
#使用redis自帶的客戶端工具redis-cli
redis-cli
#向redis寫入一個key名hello,值為world
set hello world
#讀取key名稱為hello的值
get hello
#Redis預設配置是16個數據庫,通常沒有特殊指定連線操作的是0號庫,可以通過select命令選擇庫的索引,比如可以選擇1號庫
select 1
Redis Cluster安裝(偽叢集)
我們這裡採用在同一臺上多個埠執行多個redis例項的偽叢集安裝方式(當然也可以採用之前學習的docker等容器化的方式部署redis叢集),同樣需要先安裝redis,可參考上面單機安裝步驟。
#建立叢集目錄,放置各叢集例項的配置和資料,建立六個資料夾,分別以埠號命名7000 7001 7002 7003 7004 7005六個以埠號為名字的子目錄, 稍後我們在將每個目錄中執行一個 Redis 例項
mkdir rediscluster
cd rediscluster
mkdir 7000 7001 7002 7003 7004 7005
#並將redis.conf配置檔案拷貝六個目錄下conf資料夾中,修改六個redis.conf 最少配置內容,埠port的配置和目錄資料夾名稱一致,其他內容如資料檔案目錄dir、bind、密碼等配置可以按照實際的情況需求進行修改
vi redis.conf
daemonize yes
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
#分別進入6個埠目錄
cd 7000
#分別啟動相應目錄下配置檔案的redis例項
redis-server redis.conf
#配置叢集資訊
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
redis-cli --cluster create 192.168.50.36:7000 192.168.50.36:7001 192.168.50.36:7002 192.168.50.36:7003 192.168.50.36:7004 192.168.50.36:7005 --cluster-replicas 1
出現下面的資訊則代表叢集的資訊已經配置成功
#通過客戶端登入redis叢集
redis-cli -c -p 7000
#和上面一樣讀取鍵值驗證redis叢集是否正常
Redis功能特性
常見功能
Redis命令
官網提供非常詳細資訊可以查閱,對於常見命令如所有資料結構讀寫操作命令都是需要熟悉的
也可以通過官方提供客戶端help命令查閱
Redis客戶端庫
Redis支援非常多種語言的運營,官方上列出54種程式語言庫,待黃色星號的是對應程式語言推薦的客戶端庫
以我們Java開發技術棧來說,推薦使用Jedis(一個非常小和健全的Redis Java客戶端)、Lettuce(先進的Redis客戶端執行緒安全同步,非同步,和反應使用。支援叢集、哨兵、流水線和編解碼器。後面有Lettuce官網和GitHub原始碼地址,目前很多整合框架如SpringBoot都是使用Lettuce庫)、Redisson(基於Redis伺服器的分散式協調和可擴充套件的Java資料結構,如封裝redis的分散式鎖)。後面有時間我們專門針對Lettuce、Redisson這兩個庫實戰和原理做專門剖析。
從Lettuce官網上就可以簡單示例,包括對於怎麼連線單機版本和叢集版本,當然實際上我們更多的是使用Spring與Redis整合作為開發方式。
#如使用Maven,pom.xml加入下面依賴
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.5.RELEASE</version>
</dependency>
#如使用Gradle,build.gradle加入下面依賴
dependencies {
compile 'io.lettuce:lettuce-core:6.1.5.RELEASE
}
import io.lettuce.core.*;
public class ConnectToRedis {
public static void main(String[] args) {
#Redis分為16個庫,下面使用的是0號庫
RedisClient redisClient = RedisClient.create("redis://password@localhost:6379/0");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> syncCommands = connection.sync();
syncCommands.set("testkey", "test string value");
connection.close();
redisClient.shutdown();
}
}
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
public class ConnectToRedisCluster {
public static void main(String[] args) {
// Syntax: redis://[password@]host[:port]
// Syntax: redis://[username:password@]host[:port]
RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7000");
StatefulRedisClusterConnection<String, String> connection = redisClient.connect();
System.out.println("Connected to Redis");
connection.close();
redisClient.shutdown();
}
}
Redis釋出/訂閱(Pub/Sub)
Redis釋出訂閱(pub/sub)是一種訊息通訊模式:傳送者(pub)傳送訊息,訂閱者(sub)接收訊息,客戶端訂閱到一個或多個頻道,其他客戶端發到這些頻道的訊息將會被推送到所有訂閱的客戶端;釋出/訂閱與key所在空間沒有關係,它不會受任何級別的干擾,包括不同資料庫索引, 釋出在db 10,訂閱可以在db 1。
- SUBCRIBE:訂閱一個或者多個頻道。
- PSUBCRIBE:訂閱一個或多個符合給定模式的頻道;每個模式以 * 作為匹配符,比如*itxiaoshen匹配所有以 it 開頭的頻道(news.itxiaoshen 、 sports.itxiaoshen 等等)。
- Publish:命令用於將資訊傳送到指定的頻道。
- Pubsub:命令用於檢視訂閱與釋出系統狀態。
- UNSUBCRIBE:退訂給定的一個或多個頻道的資訊。
- PUNSUBCRIBE:退訂所有給定模式的頻道。
#客戶端訂閱執行
SUBSCRIBE devchannel testchannel
PSUBSCRIBE *itxiaoshen blog*
#釋出資訊
PUBLISH testchannel hello
PUBLISH productchannel hello
PUBLISH devchannel hello
PUBLISH new.itxiaoshen hello
PUBLISH sports.itxiaoshen hello
PUBLISH sports.xiaoshen hello
PUBLISH blog.csdn hello
#檢視所有通道列表
PUBSUB CHANNELS
管道
Redis是一種基於客戶端-服務端模型以及請求/響應協議的TCP服務,客戶端向服務端傳送一個查詢請求,並監聽Socket返回,通常是以阻塞模式,等待服務端響應。服務端處理命令,並將結果返回給客戶端。
管道一次請求/響應伺服器能實現處理新的請求即使舊的請求還未被響應。這樣就可以將多個命令傳送到伺服器,而不用等待回覆,最後在一個步驟中讀取該答覆。而當執行的命令較多時,這樣的一來一回的網路傳輸所消耗的時間被稱為RTT(Round Trip Time),顯而易見,如果可以將這些命令作為一個請求一次性發送給服務端,並一次性將結果返回客戶端,會節約很多網路傳輸的消耗,可以大大提升響應時間。
大量 pipeline 應用場景可通過 Redis 指令碼(Redis 版本 >= 2.6)得到更高效的處理,後者在伺服器端執行大量工作。指令碼的一大優勢是可通過最小的延遲讀寫資料,讓讀、計算、寫等操作變得非常快(pipeline 在這種情況下不能使用,因為客戶端在寫命令前需要讀命令返回的結果)。 Redis 中的指令碼本身也就是一種事務, 所以任何在事務裡可以完成的事, 在腳本里面也能完成。
Lua指令碼
使用Lua優點
使用內建的 Lua 直譯器,可以對 Lua(Lua是一種輕量小巧的指令碼語言,用標準C語言編寫並以原始碼形式開放。其設計目的就是為了嵌入應用程式中,從而為應用程式提供靈活的擴充套件和定製功能) 指令碼進行求值,Redis Lua指令碼適合簡單快速執行的業務,如果是複雜計算業務則會阻塞Redis server端的處理業務。
- 減少網路開銷:可以將多個請求通過指令碼的形式一次傳送,減少網路時延。
- 原子性:Redis 使用單個 Lua 直譯器以原子性(atomic)的方式執行指令碼,保證 lua 指令碼在處理的過程中不會被任意其它請求打斷, 這和使用MULTI/EXEC包圍的事務很類似。
- 複用:客戶端傳送的指令碼會永久存在redis中,這樣其他客戶端可以複用這一指令碼,而不需要使用程式碼完成相同的邏輯。
EVAL命令
- EVAL的第一個引數是一段 Lua 5.1 指令碼程式。 這段Lua指令碼不需要(也不應該)定義函式。它執行在 Redis 伺服器中。
- EVAL的第二個引數是引數的個數,後面的引數(從第三個引數),表示在指令碼中所用到的那些 Redis 鍵(key),這些鍵名引數可以在 Lua 中通過全域性變數 KEYS 陣列,用 1 為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
- 在命令的最後,那些不是鍵名引數的附加引數 arg [arg …] ,可以在 Lua 中通過全域性變數 ARGV 陣列訪問,訪問的形式和 KEYS 變數類似( ARGV[1] 、 ARGV[2] ,諸如此類)
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second#使用了redis為lua內建的redis.call函式EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 good_price 99.00 300
SCRIPT 命令
#SCRIPT LOAD將一個指令碼裝入指令碼快取,但並不立即執行它
SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
#在指令碼被加入到快取之後,在任何客戶端通過EVALSHA命令,可以使用指令碼的SHA1校驗和來呼叫這個指令碼。指令碼可以在快取中保留無限長的時間,直到執行SCRIPT FLUSH為止
EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 good_stock 1000 600
#SCRIPT EXISTS根據給定的指令碼校驗和,檢查指定的指令碼是否存在於指令碼快取
SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
#SCRIPT FLUSH清除所有指令碼快取
SCRIPT FLUSH
#SCRIPT KILL殺死當前正在執行的指令碼
Lua指令碼檔案執行示例
建立mytest.lua指令碼檔案
--- 獲取key
local key = KEYS[1]
--- 獲取value
local val = KEYS[2]
--- 獲取一個引數
local expire = ARGV[1]
--- 如果redis找不到這個key就去插入
if redis.call("get", key) == false then
--- 如果插入成功,就去設定過期值
if redis.call("set", key, val) then
--- 由於lua指令碼接收到引數都會轉為String,所以要轉成數字型別才能比較
if tonumber(expire) > 0 then
--- 設定過期時間
redis.call("expire", key, expire)
end
return true
end
return false
else
return false
end
#執行mytest.lua指令碼檔案
redis-cli --eval mytest.lua myKey myValue , 100
事務
定義
Redis 事務可以一次執行多個命令,將一系列的預定義命令放入佇列,執行時按照新增順序執行,redis 的事務更像是批量執行指令,有兩個重要的保證:
- 事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端傳送來的命令請求所打斷。
- 事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。
加入事務的命令只是暫時存放在佇列中,只有在執行了 exec 指令後才會被執行
命令
- MULTI :開啟事務,redis會將後續的命令逐個放入佇列中,然後使用EXEC命令來原子化執行這個命令系列。
- EXEC:執行事務中的所有操作命令。
- DISCARD:取消事務,放棄執行事務塊中的所有命令。
- WATCH:監視一個或多個key,如果事務在執行前,這個key(或多個key)被其他命令修改,則事務被中斷,不會執行事務中的任何命令。
- UNWATCH:取消WATCH對所有key的監視。
事務錯誤處理
使用事務時可能會遇上以下兩種錯誤:
- 事務在執行 EXEC 之前,入隊的命令可能會出錯,比如說,命令可能會產生語法錯誤(引數數量錯誤,引數名錯誤,等等),或者其他更嚴重的錯誤,比如記憶體不足(如果伺服器使用 maxmemory 設定了最大記憶體限制的話);伺服器會對命令入隊失敗的情況進行記錄,並在客戶端呼叫 EXEC 命令時,拒絕執行並自動放棄這個事務。
- 命令可能在 EXEC 呼叫之後失敗。舉個例子,事務中的命令可能處理了錯誤型別的鍵,比如將列表命令用在了字串鍵上面,諸如此類。 EXEC 命令執行之後所產生的錯誤, 並沒有對它們進行特別處理: 即使事務中有某個/某些命令在執行時產生了錯誤, 事務中的其他命令仍然會繼續執行
為什麼Redis不支援事務回滾?
多數事務失敗是由語法錯誤或者資料結構型別錯誤導致的,語法錯誤說明在命令入隊前就進行檢測的,而型別錯誤是在執行時檢測的,這些Redis為提升效能而採用這種簡單的事務,這是不同於關係型資料庫的,特別要注意區分。
WATCH監視鎖
嚴格的說Redis的命令是原子性的,而事務是非原子性的,Redis WATCH命令可以讓事務具有回滾的能力。Redis使用WATCH命令來決定事務是繼續執行還是回滾,那就需要在MULTI之前使用WATCH來監控某些鍵值對,然後使用MULTI命令來開啟事務,執行對資料結構操作的各種命令,此時這些命令入佇列。當使用EXEC執行事務時,首先會比對WATCH所監控的鍵值對,如果沒發生改變,它會執行事務佇列中的命令,提交事務;如果發生變化,將不會執行事務中的任何命令,同時事務回滾。當然無論是否回滾,Redis都會取消執行事務前的WATCH命令。在WATCH之後,MULTI之前執行UNWATCH,則事務正常提交。
分散式鎖
Redisson GitHub分散式鎖使用示例 https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers
引入Redisson的依賴,然後基於Redis實現分散式鎖的加鎖與釋放鎖,實際使用中我們也會基於redisson和spring框架的整合
maven pom依賴
<!-- https://mvnrepository.com/artifact/org.redisson/redisson --><dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.16.3</version></dependency>
#配置Config config = new Config();config.useClusterServers() // use "rediss://" for SSL connection .addNodeAddress("redis://127.0.0.1:7181");#建立Redisson的例項RedissonClient redisson = Redisson.create(config);
簡單鎖的示例
RLock lock = redisson.getLock("myLock");// traditional lock methodlock.lock();// or acquire lock and automatically unlock it after 10 secondslock.lock(10, TimeUnit.SECONDS);// or wait for lock aquisition up to 100 seconds // and automatically unlock it after 10 secondsboolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);if (res) { try { ... } finally { lock.unlock(); }}
簡單紅鎖的使用示例
RReadWriteLock rwlock = redisson.getReadWriteLock("myLock");RLock lock = rwlock.readLock();// orRLock lock = rwlock.writeLock();// traditional lock methodlock.lock();// or acquire lock and automatically unlock it after 10 secondslock.lock(10, TimeUnit.SECONDS);// or wait for lock aquisition up to 100 seconds // and automatically unlock it after 10 secondsboolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);if (res) { try { ... } finally { lock.unlock(); }}
Distributed locks:用Redis實現分散式鎖管理器,分散式鎖在很多場景中是非常有用,官方提供一個使用Redis實現分散式鎖的Redlock演算法,這種實現比普通的單例項實現更安全,下面為各種語言基於Redlock演算法實現分散式鎖。
面試題
Redis分散式鎖實現思路?
- 自旋鎖:迴圈獲取鎖,類似CAS。
- 原子性:可利用redis lua指令碼的原子性。加鎖過程可以利用set nx命令SET lock_key random_value NX PX 5000,判斷key是否存在,不存在則設定key值並設定ttl時間。random_value是客戶端生成的唯一的字串(可使用雪花演算法),NX代表只在鍵不存在時,才對鍵進行設定操作,PX設定鍵的過期時間為5000毫秒這裡random_value取值作為客戶端加鎖的時間不宜過長過短。解鎖的過程就是將Key鍵刪除,但也不能亂刪,不能說客戶端1的請求執行緒裡將客戶端2的鎖給刪除掉,這時候就可以使用到random_value來實現。刪除的時候可以通過lua指令碼的原子性判斷當前請求如果是對應客戶端唯一標識字串則將key刪除。
- 鎖的延期:設定鎖的時間比如為10秒,通過類似看門狗技術檢查key的ttl值是否快要到期,重新設定或重置ttl的時間。
- 上述幾點主要是實現單臺redis分散式鎖的核心點,至於主從和叢集可以參考上述紅鎖演算法思想。
簡單談談一致性雜湊演算法和Redis雜湊槽?
一句話概括一致性雜湊:就是普通取模雜湊演算法的改良版,雜湊函式計算方法不變,只不過是通過構建環狀的 Hash 空間代替普通的線性 Hash 空間。
資料儲存的位置是沿順時針的方向找到的環上的第一個節點,資料傾斜和節點宕機都可能會導致快取雪崩。虛擬節點,就是對原來單一的物理節點在雜湊環上虛擬出幾個它的分身節點,這些分身節點稱為「虛擬節點」。打到分身節點上的資料實際上也是對映到分身對應的物理節點上,這樣一個物理節點可以通過虛擬節點的方式均勻分散在雜湊環的各個部分,解決了資料傾斜問題。
redis 叢集(cluster)並沒有使用一致性雜湊,而是採用了雜湊槽(slot)的這種概念。主要的原因是一致性雜湊演算法的節點分佈基於圓環,無法很好的手動控制資料分佈,比如一個節點失效,把資料轉移到下一個節點,容易造成快取雪崩,而採用hash槽+副本節點失效的時候從節點自動接替,不易造成雪崩。
redis cluster 包含了16384個雜湊槽,叢集使用公式 CRC16(key) % 16384 來計算鍵 key 屬於哪個槽,也即是每個 key 通過計算後都會落在具體一個槽位上,而這個槽位是屬於哪個儲存節點的,則由使用者自己定義分配,叢集中的每一個節點負責處理一部分雜湊槽。
Redis分割槽方案有哪些?
- 客戶端分割槽:由客戶端決定資料被儲存在哪個redis節點或者從哪個redis節點讀取,大部分客戶端都已實現了客戶端分割槽。
- 代理分割槽:客戶端將請求傳送給代理,有代理決定請求給哪些redis例項,然後根據Redis的響應結果返回給客戶端,像Twemproxy就是redis一種代理實現。
- 查詢路由:客戶端隨機的請求到任意一個redis例項,然後由Redis將請求轉給正確的redis例項節點。而Redis Cluster實現一種混合形式的Query routing,並不是直接將請求從一個redis節點轉發到另一個redis節點,而是在客戶端直接儲存所有例項的key儲存分佈資訊,所以在客戶端上就直接redirected到正確的redis節點。
- Redis哨兵和codis也是redis高可用的解決方案。
Redis資料型別和底層資料結構的理解?
redis底層核心實現是一個雙陣列,hash 陣列+連結串列,通過雜湊衝突解決方法如連結串列法、再雜湊。
type是約束api, object encoding是底層實現型別
資料型別:string、list、set、sortset、hash、hyperloglog、 stream 、geo
底層資料結構:雜湊表、跳錶、雙向連結串列、壓縮列表等
- 簡單字串:SDS simple dynamic string 包含free len char陣列 擴容為(len+addlen)*2,二進位制安全、記憶體預分配、相容C語言庫、空間換時間 sdshdr 長度 分配空間 型別 char陣列,獲取到是char陣列的地址,往前偏移可獲取型別進而獲取其他。
- 雜湊表:雜湊表的製作方法一般有兩種,一種是: 開放定址法,一種是 拉鍊法。redis的雜湊表的使用的是拉鍊法。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ihhvn4XI-1634137212485)(image-20211013181140096.png)]
- 雙向連結串列:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-NIeAH9WR-1634137212486)(image-20211013181213164.png)]
- 跳錶:zset是一個有序集合、排行版功能,關於時間複雜度跳錶少量資料的話誤差比較大、大量資料的話接近olog(n),zset 的長度小於128或者key的長度小於64,ziplist壓縮列表(位元組陣列),大於則使用skiplist。
-
壓縮列表: redis的列表鍵和雜湊鍵的底層實現之一。此資料結構是為了節約記憶體而開發的。和各種語言的陣列類似,它是由連續的記憶體塊組成的,這樣一來,由於記憶體是連續的,就減少了很多記憶體碎片和指標的記憶體佔用,進而節約了記憶體。
Redis擴容時機?
Redis擴容是使用兩個雜湊表分多次漸進式rehash和動態擴容機制。當used大於size擴容,排除場景包括持久化、lua事務阻塞,如果大於5size則直接擴容,翻倍擴容如4-8-16,2指數主要方便位運算,可以將取模轉為位運算,採用頭插法,當used<=size*0.1時候進行縮容;redis擴容採用漸進式rehash的方式,redis CRUD每操作一次rehash一次,每毫秒100個數組槽位。
Redis同步機制?
Redis同步機制分為全量複製和增量複製。全同步是指slave啟動時進行的初始化同步。 增量複製是指Redis執行過程中的修改同步。
- 全同步過程如下:
- 在slave啟動時,會向master傳送一條SYNC指令。
- master收到這條指令後,會啟動一個備份程序將所有資料寫到rdb檔案中去。
- 更新master的狀態(備份是否成功、備份時間等),然後將rdb檔案內容傳送給等待中的slave。
- 注意,master並不會立即將rdb內容傳送給slave。而是為每個等待中的slave註冊寫事件,當slave對應的socket可以傳送資料時,再講rdb內容傳送給slave。
- 當Redis的master/slave服務啟動後,首先進行全同步。之後,所有的寫操作都在master上,而所有的讀操作都在slave上。因此寫操作需要及時同步到所有的slave上,這種同步就是部分同步。 部分同步過程如下:
- master收到一個操作,然後判斷是否需要同步到salve。
- 如果需要同步,則將操作記錄到aof檔案中。
- 遍歷所有的salve,將操作的指令和引數寫入到savle的回覆快取中。
- 一旦slave對應的socket傳送快取中有空間寫入資料,即將資料通過socket發出去。
redis過期策略和淘汰策略、持久化機制?
-
過期策略
- 定時過期
- 惰性過期。
- 貪心策略:redis 會將每個設定了過期時間的 key 放入到一個獨立的字典中,間隔100ms隨機抽取20個key。
-
淘汰策略:Redis官方給的警告,當記憶體不足時,Redis會根據配置的快取策略淘汰部分keys,以保證寫入成功。當無淘汰策略時或沒有找到適合淘汰的key時,Redis直接返回out of memory錯誤。
- volatile-lru:從已設定過期時間的資料集(server.db[i].expires)中挑選最近最少使用的資料淘汰。
- volatile-ttl:從已設定過期時間的資料集(server.db[i].expires)中挑選將要過期的資料淘汰。
- volatile-random:從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰。
- allkeys-lru:從資料集(server.db[i].dict)中挑選最近最少使用的資料淘汰。
- allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰。
- no-enviction(驅逐):禁止驅逐資料。
-
持久化機制
- RDB快照(snapshot):在預設情況下, Redis 將記憶體資料庫快照儲存在名字為
dump.rdb
的二進位制檔案中。你可以對 Redis 進行設定, 讓它在“N 秒內資料集至少有 M 個改動”這一條件被滿足時,自動儲存一次資料集。 - AOF(append-only file):快照功能並不是非常耐久(durable): 如果 Redis 因為某些原因而造成故障停機, 那麼伺服器將丟失最近寫入、且仍未儲存到快照中的那些資料。Redis 增加了一種完全耐久的持久化方式: AOF 持久化,將修改的每一條指令記錄進檔案你可以通過修改配置檔案來開啟 AOF 功能。
appendonly yes
- 混合持久化:Redis 4.0之後帶來了一個新的持久化選項,混合持久化同樣也是通過
bgrewriteaof
完成的,不同的是當開啟混合持久化時,fork出的子程序先將共享的記憶體副本全量的以RDB方式寫入aof檔案,然後在將aof_rewrite_buf
重寫緩衝區的增量命令以AOF方式寫入到檔案,寫入完成後通知主程序更新統計資訊,並將新的含有RDB格式和AOF格式的AOF檔案替換舊的的AOF檔案。簡單的說:新的AOF檔案前半段是RDB格式的全量資料後半段是AOF格式的增量資料,如下圖
- RDB快照(snapshot):在預設情況下, Redis 將記憶體資料庫快照儲存在名字為
在redis重啟的時候,載入 aof 檔案進行恢復資料:先載入 rdb 內容再載入剩餘的 aof。混合持久化配置:
aof-use-rdb-preamble yes # yes:開啟,no:關閉
說說Redis網路IO和單執行緒為何能支援高併發?
Redis基於Reactor模式開發了網路事件處理器,這個處理器被稱為檔案事件處理器。它的組成結構為4部分:多個套接字、IO多路複用程式、檔案事件分派器、事件處理器。因為檔案事件分派器佇列的消費是單執行緒的,所以Redis才叫單執行緒模型。Redis採用網路IO多路複用技術來保證在多連線的時候,系統的高吞吐量。多路-指的是多個socket連線,複用-指的是複用一個執行緒。多路複用主要有三種技術:select,poll,epoll。epoll是最新的也是目前最好的多路複用技術。這裡“多路”指的是多個網路連線,“複用”指的是複用同一個執行緒。採用多路I/O複用技術可以讓單個執行緒高效的處理多個連線請求(儘量減少網路IO的時間消耗),且Redis在記憶體中操作資料的速度非常快(記憶體內的操作不會成為這裡的效能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。
Redis採用單執行緒為何支援高併發?
- Redis使用的記憶體IO,不是磁碟IO,大大降低了IO時間
- Redis單執行緒,無需去考慮多執行緒造成的死鎖問題
- Redis單執行緒,底層網路IO模型使用多路複用epoll方式(如果核心不支援epoll,可自動切換到select或者poll,看配置資訊可進行修改)
Redis6實現的多執行緒,只是對網路IO讀寫處理做多執行緒處理,但是對命令列的操作仍然是單執行緒的。這樣即加快了IO處理效率,又保證了原子性。
簡單說說Redis協議?
Redis客戶端和服務端之間使用一種名為RESP(REdis Serialization Protocol)的二進位制安全文字協議進行通訊,屬於請求-響應模型。
#用SET命令來舉例說明RESP協議的格式。
SET mykey "Hello"
#實際傳送的請求資料:
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n
#實際收到的響應資料:
+OK\r\n
RESP設計的十分精巧,下面是一張完備的協議描述圖。
請說說對於快取預熱、快取穿透、快取雪崩、快取擊穿、快取更新、快取降級的理解?
-
快取穿透
-
定義
- 當查詢Redis中沒有的資料時,該查詢會下沉到資料庫層,同時資料庫層也沒有該資料,當這種情況大量出現或被惡意攻擊時,介面的訪問全部透過Redis訪問資料庫,而資料庫中也沒有這些資料,我們稱這種現象為"快取穿透"。快取穿透會穿透Redis的保護,提升底層資料庫的負載壓力,同時這類穿透查詢沒有資料返回也造成了網路和計算資源的浪費。
-
解決方案:
- 在介面訪問層對使用者做校驗,如介面傳參、登陸狀態、n秒內訪問介面的次數;
- 利用布隆過濾器,將資料庫層有的資料key儲存在位陣列中,以判斷訪問的key在底層資料庫中是否存在;核心思想是布隆過濾器,在redis裡也有bitmap點陣圖的類似實現,布隆過濾器過濾器不能實現動態刪除,有時間可以研究下布穀鳥過濾器,是布隆過濾器增強版本。布隆過濾器有誤判率,雖然不能完全避免資料穿透的現象,但已經可以將99.99%的穿透查詢給遮蔽在Redis層了,極大的降低了底層資料庫的壓力,減少了資源浪費。
- 基於布隆過濾器,我們可以先將資料庫中資料的key儲存在布隆過濾器的位陣列中,每次客戶端查詢資料時先訪問Redis:
- 如果Redis內不存在該資料,則通過布隆過濾器判斷資料是否在底層資料庫內;
- 如果布隆過濾器告訴我們該key在底層庫內不存在,則直接返回null給客戶端即可,避免了查詢底層資料庫的動作;
- 如果布隆過濾器告訴我們該key極有可能在底層資料庫記憶體在,那麼將查詢下推到底層資料庫即可;
-
-
快取擊穿
-
定義
- 快取擊穿和快取穿透從名詞上可能很難區分開來,它們的區別是:穿透表示底層資料庫沒有資料且快取內也沒有資料,擊穿表示底層資料庫有資料而快取內沒有資料。當熱點資料key從快取內失效時,大量訪問同時請求這個資料,就會將查詢下沉到資料庫層,此時資料庫層的負載壓力會驟增,我們稱這種現象為"快取擊穿"。
-
解決方案
- 延長熱點key的過期時間或者設定永不過期,如排行榜,首頁等一定會有高併發的介面;
- 利用互斥鎖保證同一時刻只有一個客戶端可以查詢底層資料庫的這個資料,一旦查到資料就快取至Redis內,避免其他大量請求同時穿過Redis訪問底層資料庫;
-
-
快取雪崩
- 定義
- 快取雪崩是快取擊穿的"大面積"版,快取擊穿是資料庫快取到Redis內的熱點資料失效導致大量併發查詢穿過redis直接擊打到底層資料庫,而快取雪崩是指Redis中大量的key幾乎同時過期,然後大量併發查詢穿過redis擊打到底層資料庫上,此時資料庫層的負載壓力會驟增,我們稱這種現象為"快取雪崩"。事實上快取雪崩相比於快取擊穿更容易發生,對於大多數公司來講,同時超大併發量訪問同一個過時key的場景的確太少見了,而大量key同時過期,大量使用者訪問這些key的機率相比快取擊穿來說明顯更大。
- 解決方案
- 在可接受的時間範圍內隨機設定key的過期時間,分散key的過期時間,以防止大量的key在同一時刻過期;
- 對於一定要在固定時間讓key失效的場景(例如每日12點準時更新所有最新排名),可以在固定的失效時間時在介面服務端設定隨機延時,將請求的時間打散,讓一部分查詢先將資料快取起來;
- 延長熱點key的過期時間或者設定永不過期,這一點和快取擊穿中的方案一樣;
- 定義
-
快取預熱
- 如字面意思,當系統上線時,快取內還沒有資料,如果直接提供給使用者使用,每個請求都會穿過快取去訪問底層資料庫,如果併發大的話,很有可能在上線當天就會宕機,因此我們需要在上線前先將資料庫內的熱點資料快取至Redis內再提供出去使用,這種操作就成為"快取預熱"。
- 快取預熱的實現方式有很多,比較通用的方式是寫個批任務,在啟動專案時或定時去觸發將底層資料庫內的熱點資料載入到快取內。
-
快取降級
- 快取降級是指當訪問量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的效能時,即使是有損部分其他服務,仍然需要保證主服務可用。可以將其他次要服務的資料進行快取降級,從而提升主服務的穩定性。
- 降級的目的是保證核心服務可用,即使是有損的。如去年雙十一的時候淘寶購物車無法修改地址只能使用預設地址,這個服務就是被降級了,這裡阿里保證了訂單可以正常提交和付款,但修改地址的服務可以在伺服器壓力降低,併發量相對減少的時候再恢復。
- 降級可以根據實時的監控資料進行自動降級也可以配置開關人工降級。是否需要降級,哪些服務需要降級,在什麼情況下再降級,取決於大家對於系統功能的取捨。
-
快取更新
- 快取服務(Redis)和資料服務(底層資料庫)是相互獨立且異構的系統,在更新快取或更新資料的時候無法做到原子性的同時更新兩邊的資料,因此在併發讀寫或第二步操作異常時會遇到各種資料不一致的問題。如何解決併發場景下更新操作的雙寫一致是快取系統的一個重要知識點。
Redis快取資料如何保證與Mysql的一致性
採用延時雙刪策略,在寫庫前後都進行redis.del(key)操作,並且設定合理的超時時間。具體步驟是:
-
先刪除快取
-
再寫資料庫
-
休眠N毫秒(根據具體的業務時間來定)
-
再次刪除快取。
-
休眠N毫秒怎麼確定
- 需要評估自己的專案的讀資料業務邏輯的耗時。這麼做的目的,就是確保讀請求結束,寫請求可以刪除讀請求造成的快取髒資料。當然,這種策略還要考慮 redis 和資料庫主從同步的耗時。最後的寫資料的休眠時間:則在讀資料業務邏輯的耗時的基礎上,加上幾百ms即可。比如:休眠1秒
-
設定快取的過期時間
- 從理論上來說,給快取設定過期時間,是保證最終一致性的解決方案。所有的寫操作以資料庫為準,只要到達快取過期時間,則後面的讀請求自然會從資料庫中讀取新值然後回填快取
- 結合雙刪策略+快取超時設定,這樣最差的情況就是在超時時間內資料存在不一致,而且又增加了寫請求的耗時
-
如何寫完資料庫後,保證再次刪除快取成功?
- 上述的方案有一個缺點,那就是操作完資料庫後,由於種種原因刪除快取失敗,這時,可能就會出現資料不一致的情況。這裡,我們需要提供一個保障重試的方案。
- 方案一具體流程(但該方案有一個缺點,對業務線程式碼造成大量的侵入)
- 更新資料庫資料;
- 快取因為種種問題刪除失敗;
- 將需要刪除的key傳送至訊息佇列;
- 自己消費訊息,獲得需要刪除的key;
- 繼續重試刪除操作,直到成功。
- 方案二具體流程
- 更新資料庫資料;
- 資料庫會將操作資訊寫入binlog日誌當中;
- 訂閱程式提取出所需要的資料以及key;
- 另起一段非業務程式碼,獲得該資訊;
- 嘗試刪除快取操作,發現刪除失敗;
- 將這些資訊傳送至訊息佇列;
- 重新從訊息佇列中獲得該資料,重試操作。
以上方案都是在業務中經常會碰到的場景,可以依據業務場景的複雜和對資料一致性的要求來選擇具體的方案
redis技術點非常多,本章主要對redis有一個全域性的理解,後續有時間我們再深入理解redis其他內容,包括客戶端框架原理和運用。