Redis[快問快答系列]

豎橫山發表於2023-02-23

什麼是 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 為 56789
    > SADD uid:1 5 6 7 8 9
    # uid:2 使用者關注公眾號 id 為 7891011
    > 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
  • 電話和姓名排序
    使用有序集合的 ZRANGEBYLEXZREVRANGEBYLEX 可以幫助我們實現電話號碼或姓名的排序

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 中。
    PFADD page1:uv user1 
    PFADD page1:uv user2
    用 PFCOUNT 命令直接獲得 page1 的 UV 值了
    PFCOUNT page1:uv
GEO:

GEO 本身並沒有設計新的底層資料結構,而是直接使用了 Sorted Set 集合型別。

  • 查詢使用者附近的網約車
    把 ID 號為 33 的車輛的當前經緯度位置存入 GEO 集合中:
    GEOADD cars:locations 116.034579 39.030452 33
    當使用者想要尋找自己經緯度(116.054579,39.030452 )為中心的5公里內的車輛資訊
    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 協議》,轉載必須註明作者和本文連結
遇強則強,太強另說

相關文章