什麼是 Redis?
Redis 是一種基於記憶體的資料庫,對資料的讀寫操作都是在記憶體中完成,因此讀寫速度非常快,常用於快取,訊息佇列、分散式鎖等場景。
Redis 提供了多種資料型別來支援不同的業務場景,比如 String(字串)、Hash(雜湊)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(點陣圖)、HyperLogLog(基數統計)、GEO(地理資訊)、Stream(流),並且對資料型別的操作都是原子性的,因為執行命令由單執行緒負責的,不存在併發競爭的問題。
除此之外,Redis 還支援事務 、持久化、Lua 指令碼、多種叢集方案(主從複製模式、哨兵模式、切片機群模式)、釋出/訂閱模式,記憶體淘汰機制、過期刪除機制等等。
Redis 和 Memcached 有什麼區別?
- Memcached 只支援最簡單的 key-value 資料型別
- Redis 支援資料的持久化,Memcached 重啟或者掛掉後,資料就沒了
- Redis 原生支援叢集模式,Memcached 沒有原生的叢集模式
- Redis 支援釋出訂閱模型、Lua 指令碼、事務等功能,而 Memcached 不支援
為什麼用 Redis 作為 MySQL 的快取?
Redis具備高效能,高併發,Redis 單機的 QPS 能輕鬆破 10w,而 MySQL 單機的 QPS 很難破 1w。
Redis 資料型別以及使用場景分別是什麼?
String:
- 快取物件:
SET user:1 '{"name":"xiaolin", "age":18}'
- 計數器:
INCR count:1001
- 分散式鎖:
SET lock_key unique_value NX PX 10000
- 共享session:適用分散式系統
List:
- 訊息佇列:
訊息保序:使用 LPUSH + RPOP;
阻塞讀取:使用 BRPOP;
重複訊息處理:生產者自行實現全域性唯一 ID;
訊息的可靠性:使用 BRPOPLPUSH
Hash:
- 快取物件
一般物件用 String + Json 儲存,物件中某些頻繁變化的屬性可以考慮抽出來用 Hash 型別儲存。# 儲存一個雜湊表uid:1的鍵值 > HMSET uid:1 name Tom age 15 2 # 儲存一個雜湊表uid:2的鍵值 > HMSET uid:2 name Jerry age 13 2 # 獲取雜湊表使用者id為1中所有的鍵值 > HGETALL uid:1 1) "name" 2) "Tom" 3) "age" 4) "15"
- 購物車
新增商品:HSET cart:{使用者id} {商品id} 1 新增數量:HINCRBY cart:{使用者id} {商品id} 1 商品總數:HLEN cart:{使用者id} 刪除商品:HDEL cart:{使用者id} {商品id} 獲取購物車所有商品:HGETALL cart:{使用者id}
Set:
聚合計算場景
主從叢集中,為了避免主庫因為 Set 做聚合計算(交集、差集、並集)時導致主庫被阻塞,我們可以選擇一個從庫完成聚合統計點贊
#`uid:1` 使用者對文章 article:1 點贊 SADD article:1 uid:1
#`uid:1` 取消了對 article:1 文章點贊。 SREM article:1 uid:1
# 獲取 article:1 文章所有點贊使用者 : SMEMBERS article:1 1) "uid:3" 2) "uid:2"
獲取 article:1 文章的點贊使用者數量: SCARD article:1 (integer) 2
#判斷使用者 uid:1 是否對文章 article:1 點讚了: SISMEMBER article:1 uid:1 (integer) 0 # 返回0說明沒點贊,返回1則說明點讚了
共同關注
Set 型別支援交集運算,所以可以用來計算共同關注的好友、公眾號等。# uid:1 使用者關注公眾號 id 為 5、6、7、8、9 > SADD uid:1 5 6 7 8 9 # uid:2 使用者關注公眾號 id 為 7、8、9、10、11 > SADD uid:2 7 8 9 10 11
# 獲取共同關注 > SINTER uid:1 uid:2 1) "7" 2) "8" 3) "9"
# 給 `uid:2` 推薦 `uid:1` 關注的公眾號: > SDIFF uid:1 uid:2 1) "5" 2) "6"
# 驗證某個公眾號是否同時被 `uid:1` 或 `uid:2` 關注: > SISMEMBER uid:1 5 (integer) 1 # 返回0,說明關注了 > SISMEMBER uid:2 5 (integer) 0 # 返回0,說明沒關注
抽獎活動
儲存某活動中中獎的使用者名稱 ,Set 型別因為有去重功能,可以保證同一個使用者不會中獎兩次。
key為抽獎活動名,value為員工名稱,把所有員工名稱放入抽獎箱 :>SADD lucky Tom Jerry John Sean Marry Lindy Sary Mark (integer) 5
如果允許重複中獎,可以使用 SRANDMEMBER 命令。
# 抽取 1 個一等獎: > SRANDMEMBER lucky 1 1) "Tom" # 抽取 2 個二等獎: > SRANDMEMBER lucky 2 1) "Mark" 2) "Jerry" # 抽取 3 個三等獎: > SRANDMEMBER lucky 3 1) "Sary" 2) "Tom" 3) "Jerry"
如果不允許重複中獎,可以使用 SPOP 命令。
# 抽取一等獎1個 > SPOP lucky 1 1) "Sary" # 抽取二等獎2個 > SPOP lucky 2 1) "Jerry" 2) "Mark" # 抽取三等獎3個 > SPOP lucky 3 1) "John" 2) "Sean" 3) "Lindy"
Zset:
排行榜
# arcticle:1 文章獲得了200個贊 ZADD user:xiaolin:ranking 200 arcticle:1 # 文章 arcticle:1 新增一個贊 ZINCRBY user:xiaolin:ranking 1 arcticle:1 # 檢視某篇文章的贊數 ZSCORE user:xiaolin:ranking arcticle:4 # 獲取文章贊數最多的 3 篇文章 ZREVRANGE user:xiaolin:ranking 0 2 WITHSCORES # 獲取100贊到200 讚的文章 ZRANGEBYSCORE user:xiaolin:ranking 100 200 WITHSCORES
電話和姓名排序
使用有序集合的ZRANGEBYLEX
或ZREVRANGEBYLEX
可以幫助我們實現電話號碼或姓名的排序
BitMap:
簽到
第一步,執行下面的命令,記錄該使用者 6 月 3 號已簽到。SETBIT uid:sign:100:202206 2 1
第二步,檢查該使用者 6 月 3 日是否簽到。
GETBIT uid:sign:100:202206 2
第三步,統計該使用者在 6 月份的簽到次數。
BITCOUNT uid:sign:100:202206
使用者登入狀態
第一步,執行以下指令,表示使用者已登入。SETBIT login_status 10086 1
第二步,檢查該使用者是否登陸,返回值 1 表示已登入。
GETBIT login_status 10086
第三步,登出,將 offset 對應的 value 設定成 0。
SETBIT login_status 10086 0
布隆過濾器
HyperLogLog:
只需要花費 12 KB 記憶體,就可以計算接近 2^64 個元素的基數,統計結果是有一定誤差的,標準誤算率是 0.81%。
- 百萬計網頁UV計數
在統計 UV 時,你可以用 PFADD 命令把訪問頁面的每個使用者都新增到 HyperLogLog 中。
用 PFCOUNT 命令直接獲得 page1 的 UV 值了PFADD page1:uv user1 PFADD page1:uv user2
PFCOUNT page1:uv
GEO:
GEO 本身並沒有設計新的底層資料結構,而是直接使用了 Sorted Set 集合型別。
- 查詢使用者附近的網約車
把 ID 號為 33 的車輛的當前經緯度位置存入 GEO 集合中:
當使用者想要尋找自己經緯度(116.054579,39.030452 )為中心的5公里內的車輛資訊GEOADD cars:locations 116.034579 39.030452 33
GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
Stream:
- 訊息佇列 比list高階
Redis 執行緒模型
Redis 單執行緒指的是「接收客戶端請求->解析請求 ->進行資料讀寫等操作->傳送資料給客戶端」這個過程是由一個執行緒(主執行緒)來完成的,這也是我們常說 Redis 是單執行緒的原因。
Redis程式不是單執行緒的,後臺還會有三個執行緒處理關閉檔案,AOF刷盤,釋放記憶體
Redis 採用單執行緒為什麼還這麼快?
- Redis 採用單執行緒模型可以避免了多執行緒之間的競爭,省去了多執行緒切換帶來的時間和效能上的開銷,而且也不會導致死鎖問題。
- Redis 採用了 I/O Epoll多路複用機制處理大量的客戶端 Socket 請求
在 Redis 6.0 版本之後,也採用了多個 I/O 執行緒來處理網路請求,**但是對於命令的執行,Redis 仍然使用單執行緒來處理,Redis 6.0 版本引入的多執行緒 I/O 特性對效能提升至少是一倍以上。
Redis 如何實現資料不丟失?
- AOF 日誌:每執行一條寫操作命令,就把該命令以追加的方式寫入到一個檔案裡;
- RDB 快照:將某一時刻的記憶體資料,以二進位制的方式寫入磁碟;
- 混合持久化方式:Redis 4.0 新增的方式,整合了 AOF 和 RBD 的優點;
AOF 日誌是如何實現的?
Redis 在執行完一條寫操作命令後,就會把該命令以追加的方式寫入到一個檔案裡,然後 Redis 重啟時,會讀取該檔案記錄的命令,然後逐一執行命令的方式來進行資料恢復。
Redis 提供了 3 種AOF寫回硬碟的策略,在 Redis.conf 配置檔案中的 appendfsync 配置項
- Always,每次寫操作命令執行完後,同步將 AOF 日誌資料寫回硬碟;
- Everysec,每次寫操作命令執行完後,先將命令寫入到 AOF 檔案的核心緩衝區,然後每隔一秒將緩衝區裡的內容寫回到硬碟;
- No,意味著不由 Redis 控制寫回硬碟的時機,由作業系統決定何時將緩衝區內容寫回硬碟。
AOF 日誌過大,會觸發壓縮機制 bgrewriteaof
RDB 做快照時會阻塞執行緒嗎?
- 執行了 save 命令,就會在主執行緒生成 RDB 檔案,由於和執行操作命令在同一個執行緒,會阻塞主執行緒;
- 執行了 bgsave 命令,會建立一個子程式來生成 RDB 檔案,這樣可以避免主執行緒的阻塞;
Redis 還可以透過配置檔案的選項來實現每隔一段時間自動執行一次 bgsave 命令
save 900 1 //900 秒之內,對資料庫進行了至少 1 次修改;
save 300 10 //300 秒之內,對資料庫進行了至少 10 次修改;
save 60 10000 // 60 秒之內,對資料庫進行了至少 10000 次修改。
RDB 在執行快照的時候,資料能修改嗎?
可以的,執行 bgsave 過程中,Redis 依然可以繼續處理操作命令的,也就是資料是能被修改的,關鍵的技術就在於多程式的寫時複製技術(Copy-On-Write, COW)。
為什麼會有混合持久化?
Redis 4.0 提出了混合持久化,既保證了 Redis 重啟速度,又降低資料丟失風險。
Redis 如何實現服務高可用?
- 主從複製:一主多從的模式,且主從伺服器之間採用的是「讀寫分離」的方式。
- 哨兵模式:主從伺服器出現故障當機時,需要手動進行恢復。所以Redis 增加了哨兵模式,因為哨兵模式做到了可以監控主從伺服器,並且提供主從節點故障轉移的功能。
- Redis Cluster:分散式叢集,採用雜湊槽,來處理資料和節點之間的對映關係
Redis 使用的過期刪除策略是什麼?
Redis 使用的過期刪除策略是「惰性刪除+定期刪除」這兩種策略配和使用。
- 惰性刪除策略的做法是,不主動刪除過期鍵,每次從資料庫訪問 key 時,都檢測 key 是否過期,如果過期則刪除該 key。
- 定期刪除策略的做法是,每隔一段時間「隨機」從資料庫中取出一定數量的 key 進行檢查,並刪除其中的過期key。
Redis 主從模式中,對過期鍵會如何處理?
主庫在 key 到期時,會在 AOF 檔案裡增加一條 del 指令,同步到所有的從庫
Redis 記憶體滿了,會發生什麼?
在 Redis 的執行記憶體達到了配置項設定的 maxmemory,就會觸發記憶體淘汰機制
Redis 記憶體淘汰策略有哪些?
- noeviction:預設的記憶體淘汰策略,不淘汰任何資料,而是不再提供服務,直接返回錯誤。
在設定了過期時間的資料中進行淘汰
- volatile-random:隨機淘汰設定了過期時間的任意鍵值;
- volatile-ttl:優先淘汰更早過期的鍵值。
- volatile-lru:淘汰所有設定了過期時間的鍵值中,最久未使用的鍵值;
- volatile-lfu:淘汰所有設定了過期時間的鍵值中,最少使用的鍵值;
在所有資料範圍內進行淘汰:
- allkeys-random:隨機淘汰任意鍵值;
- allkeys-lru:淘汰整個鍵值中最久未使用的鍵值;
- allkeys-lfu:淘汰整個鍵值中最少使用的鍵值。
如何避免快取雪崩?
- 將快取失效時間隨機打散,在原有的失效時間基礎上增加一個隨機值
- 設定快取不過期,透過業務邏輯來更新快取資料
如何避免快取擊穿
- 互斥鎖方案(Redis 中使用 SET EX NX)
- 不給熱點資料設定過期時間,由後臺非同步更新快取
如何避免快取穿透
- 判斷求請求引數是否合理,請求引數是否含有非法值、請求欄位是否存在,如果判斷出是惡意請求就直接返回錯誤
- 可以針對查詢的資料,在快取中設定一個空值或者預設值返回給應用,而不會繼續查詢資料庫。
- 使用布隆過濾器快速判斷資料是否存在,避免透過查詢資料庫來判斷資料是否存在
Redis 如何實現延遲佇列?
在 Redis 可以使用有序集合(ZSet)的方式來實現延遲訊息佇列的,ZSet 有一個 Score 屬性可以用來儲存延遲執行的時間。
zadd score1 value1 命令就可以一直往記憶體中生產訊息。 zrangebyscore 查詢符合條件的所有待處理的任務, 透過迴圈執行佇列任務即可。
Redis 的大 key 如何處理?
一般而言,下面這兩種情況被稱為大 key:
- String 型別的值大於 10 KB;
- Hash、List、Set、ZSet 型別的元素的個數超過 5000個;
//最好選擇在從節點上執行該命令。因為主節點上執行時,會阻塞主節點,只能返回每種型別中最大的那個 bigkey,無法得到大小排在前 N 位的 bigkey,對於集合型別來說,只統計集合元素個數的多少,而不是實際佔用的記憶體量。
redis-cli -h 127.0.0.1 -p6379 -a "password" -- bigkeys
scan命令,配合key型別再用對應的命令計算記憶體
//使用 RdbTools 第三方開源工具,可以用來解析 Redis 快照(RDB)檔案,找到其中的大 key。
rdb dump.rdb -c memory --bytes 10240 -f redis.csv
如何刪除大 key?
- 分批次刪除
- 非同步刪除(Redis 4.0版本以上)推薦使用
從 Redis 4.0 版本開始,可以採用非同步刪除法,用 unlink 命令代替 del 來刪除。這樣 Redis 會將這個 key 放入到一個非同步執行緒中進行刪除,這樣不會阻塞主執行緒。我們還可以透過配置引數,達到某些條件的時候自動進行非同步刪除。
Redis 管道有什麼用?
把多條命令拼接到一起,當成一次請求發出去,結果也是拼接到一起發回來,免去了每條命令執行後都要等待的情況,從而有效地提高了程式的執行效率。
Redis 事務支援回滾嗎?
Redis 中並沒有提供回滾機制
如何用 Redis 實現分散式鎖的?
SET lock_key unique_value NX PX 10000
- lock_key 就是 key 鍵;
- unique_value 是客戶端生成的唯一的標識,區分來自不同客戶端的鎖操作;
- NX 代表只在 lock_key 不存在時,才對 lock_key 進行設定操作;
- PX 10000 表示設定 lock_key 的過期時間為 10s,這是為了避免客戶端發生異常而無法釋放鎖。
解鎖需要Lua指令碼保證原子性
// 釋放鎖時,先比較 unique_value 是否相等,避免鎖的誤釋放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
基於 Redis 實現分散式鎖有什麼缺點?
- 超時時間不好設定。
- 叢集情況下的不可靠性。
Redis 如何解決叢集情況下分散式鎖的可靠性?
為了保證叢集環境下分散式鎖的可靠性,Redis 官方已經設計了一個分散式鎖演演算法 Redlock(紅鎖)。它是基於多個 Redis 節點的分散式鎖,官方推薦是至少部署 5 個 Redis 節點,而且都是主節點
為什麼用跳錶而不用平衡樹?
- 從記憶體佔用上來比較,跳錶比平衡樹更靈活一些。
- 在做範圍查詢的時候,跳錶比平衡樹操作要簡單
- 從演演算法實現難度上來比較,跳錶比平衡樹要簡單得多
如何保證快取和資料庫資料的一致性?
更新資料庫 + 更新快取
如果我們的業務對快取命中率有很高的要求,我們可以採用「更新資料庫 + 更新快取」的方案,但是在兩個更新請求併發執行的時候,會出現資料不一致的問題
所以我們得增加一些手段來解決這個問題,這裡提供兩種做法:- 在更新快取前先加個分散式鎖,保證同一時間只執行一個請求更新快取,當然對於寫入的效能就會帶來影響。
- 在更新完快取時,給快取加上較短的過期時間,快取的資料也會很快過期,
先刪除快取 + 更新資料庫
延遲雙刪#刪除快取 redis.delKey(X) #更新資料庫 db.update(X) #睡眠 Thread.sleep(N) #再刪除快取 redis.delKey(X)
本作品採用《CC 協議》,轉載必須註明作者和本文連結