天天看點

Redis 基礎

Redis 特性:

  • 速度快,資料在記憶體中,通過 key 查找,時間複雜度 O(1)
  • 支援多種資料類型,string,list,hash,set,sort set 等
  • 支援事物,操作都是原子性的
  • 豐富的特性,可用于緩存等

Redis 是單線程還多線程?

答:Redis基于Reactor模式開發了網絡事件處理器,這個處理器被稱為檔案事件處理器。它的組成結構為4部分:多個套接字、IO 多路複用程式、檔案事件分派器、事件處理器。因為檔案事件分派器隊列的消費是單線程的,是以Redis才叫單線程模型。

參考和圖檔連結:

https://www.jianshu.com/p/6264fa82ac33
Redis 基礎

1、Redis 基礎

1.1 為什麼要使用 Redis?

當系統通路量大的時候,比如某個商品促銷導緻訂單量激增(比如秒殺場景),如果直接在 MySQL 扣減庫存很容易導緻 MySQL 崩掉,是以需要一個 Redis 這樣的緩存中間件。

1.2 常用的緩存中間件有那些?

知道的有 Redis 和 Memcache

  • 共同點:都是記憶體資料庫,
  • 不同點:Redis 支援持久化寫入到磁盤,而 Mencache 挂掉後就消失無法恢複。

1.3 Redis 有那些資料結構

Redis 的基本資料結構如下:

Redis 基礎

圖檔和參考來源:

https://www.cnblogs.com/haoprogrammer/p/11065461.html
  • String :存儲字元串,比較浪費記憶體,不推薦。可以用來 Session 共享,分布式鎖等;
  • Hash:比 String 省記憶體,可以比較直覺的緩存多元資訊,而 String 需要通過 JSON 等形式存儲多元資訊
  • List:連結清單,異步隊列需要延後處理的任務塞進清單,存儲微網誌、朋友圈的時間軸清單
  • Set:去重,點贊,還有收藏等
  • Sort Set:去重,有個 score,可以用來排名等

1.4、Redis 資料過期時間過于集中會導緻那些問題?

會導緻卡頓,解決方法在設定過期時間的時候加一個随機值。使得過期時間分散一點。消息過期太集中容易導緻緩存雪崩。

1.5 Redis 分布式鎖,需要主要什麼?

更多參考 Spring Boot 和 Redis 的分布式鎖:

https://ylooq.gitee.io/learn-spring-boot-2/#/12-DistributedLock

同一商品的資訊、促銷優惠等,可能會被多個人同時更改,如果沒有加分布式鎖會導緻資料前後不一緻的問題。

# 設定 鎖的指令,正确示範
SET key value [EX seconds] [PX milliseconds] [NX|XX]           

SET 參數說明,從 Redis 2.6.12 版本開始:

  • 沒有 EX、NX、PX、XX 的情況下,如果 key 已經存在,則覆寫 value 值不管其什麼類型;如果不存在,則建立一個 key -- value
  • EX seconds

    : 設定過期時間為 seconds 秒,

    SET key value EX second

    效果等同于

    SETEX key second value

  • PX millisecond

    :設定鍵的過期時間為

    millisecond

    毫秒。

    SET key value PX millisecond

    PSETEX key millisecond value

  • NX

    :隻在 key 不存在時,才對鍵進行設定操作。

    SET key value NX

    SETNX key value

  • XX

    :隻在 key 已經存在時,才對鍵進行設定操作。
  • value 值:需要加入随機的字元串,作為釋放鎖的唯一密碼(Token),預防持有過期鎖的線程誤删其他持線程的鎖

需要注意:設定鎖的時候不能先設定 key,然後在設定過期時間,如果執行兩個指令之間 Redis 程序發生意外(crash,重新開機維護)了,就會導緻鎖無法自動釋放

# 設定 key value ,這是錯誤的
SET key value
# 然後再設定過期時間 10s,萬一中間伺服器抽風了,那就沒法自動釋放鎖,容易導緻死鎖了
EXPIRE key 10           

如果在代碼,參考如下設定設定,同時設定 key vlaue 和過期時間:

# 加鎖成功,lockStat !=null && lockStat == true
Boolean lockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
                connection.set(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8),
                 Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));           

需要注意:釋放鎖的時候不能直接 DEL,要使用 lua 腳本驗證 key 和 value,一緻才能釋放鎖,可能導緻先前持有過期鎖的線程誤删除鎖。

if redis.call('get', KEYS[1]) == ARGV[1] 
  then return redis.call('del', KEYS[1]) 
    else return 0 end           

需要注意:加鎖失敗的處理

  • 抛出異常
  • sleep,然後再次嘗試
  • 使用延時隊列

1.6 Redis 的 keys 指令和 sacn 指令有什麼差別?

  • keys abc*

    : keys 可以比對後面的正規表達式,例如列出所有 abc 開頭的 key
  • scan 0【遊标】 MATCH abc* count 1【傳回多少條】

    :列出所有 abc 開頭的 key

參考 :

http://doc.redisfans.com/key/scan.html

keys 會阻塞,一直到所有符合條件的 key 列出來,如果key 的總數或者符合條件的 key 過多會導緻卡頓,而 SCAN 是增量式疊代指令,每次從遊标開始,遊标參數被設定為

時, 伺服器将開始一次新的疊代, 而當伺服器向使用者傳回值為

的遊标時, 表示疊代已結束。

不過,增量式疊代指令也不是沒有缺點的:舉個例子, 使用 SMEMBERS 指令可以傳回集合鍵目前包含的所有元素, 但是對于 SCAN 這類增量式疊代指令來說, 因為在對鍵進行增量式疊代的過程中, 鍵可能會被修改, 是以增量式疊代指令隻能對被傳回的元素提供有限的保證 。
Redis> keys key* # 測試資料
1)key1
*********
10000)key10000
redis>scan 0 match key* count 100 
# 0 表示開始疊代的遊标,match key* 正規表達式比對,count 100 傳回 100 條
1)120  #---> 下一次使用的遊标
2)1) "key233" #--->傳回的結果,不一定按照 key1 到 key10000 的順序
  2) "key1000"
  3) "key330"
  4) "key23"           

1、如果 redis 中有 1 億個 key,其中有 10w 個 abc 開頭的 key,怎麼列出來? 用 keys 指令

2、如果 redis 正在提供服務,keys 會導緻什麼後果?卡頓,可以通過 sacn 指令解決,但 scan 指令傳回的結果不能完全保證,因為在增量疊代的時候 key 可能會被修改。

1.7 Redis 如何實作消息隊列?

在 Redis 中,有個 list 的資料結構,通過如下指令生産消息和消費消息,List 是雙連結清單,遵循先入先出的原則,通過 lpush/rpush 和 rpop/lpop 釋出和消費消息。

# 消息入隊,L 左邊,R 右邊,PUSH 入隊,POP 出隊
redis> LPUSH languages python
redis> LPUSH languages java
# 消息出隊,傳回 python ,如果隊列裡面為空,則傳回 nil
redis> RPOP languages           

隻能被 1 個消費端消費,LPOP 指令在隊列為空的時候傳回 null,如果一直沒有消息,可以讓消費者線程睡睡覺 sleep 免得浪費資源。

如果消費者線程不使用 sleep() ,該怎麼辦?使用 BLPOP / 或者 BRPOP 指令,會阻塞到消息隊列有消息才傳回。

1.8 Redis 如何實作釋出、訂閱模式?也就是 1:N 消費?

使用 Redis 的 pub/sub 釋出定義模式,但不推薦使用,因為消息者下線的情況下,消息會丢失。推薦使用專門的消息中間件如 RocketMQ,RabbitMQ,Kafka。

訂閱某個 key 的消息:

http://doc.redisfans.com/pub_sub/subscribe.html
# 訂閱 msg 頻道
# 1 - 3 行是執行 subscribe 之後的回報資訊
# 第 4 - 6 行才是接收到的第一條資訊
redis> subscribe msg
Reading messages... (press Ctrl-C to quit)
1) "subscribe"       # 傳回值的類型:顯示訂閱成功
2) "msg"             # 訂閱的頻道名字
3) (integer) 1       # 目前已訂閱的頻道數量

1) "message"         # 傳回值的類型:資訊
2) "msg"             # 來源(從那個頻道發送過來)
3) "hello moto"      # 資訊内容           

釋出某個 key 的消息:

http://doc.redisfans.com/pub_sub/publish.html
redis> publish msg "good morning"
(integer) 1 # 傳回目前 msg 主題的消息訂閱用戶端數量           

1.9 Redis 如何實作延遲隊列?

如果是多線程環境處理延遲隊列,可以通過 zrangebyscore 和 zrem 一同挪到伺服器端使用 lua 腳本進行原子操作。
  • zrangebyscore 隻取一條資料
  • zrem 移除該條資料

使用 Sorted Set 資料類型,使用時間戳作為分數

# 添加隊列任務
redis> ZADD key score(時間戳) value           

擷取 N 秒前的資料

# 使用 ZRANGEBYSORT 擷取前 N 秒的資料 ,0,1 類似 MySQL limit,偏移量 0,取一條
redis> ZRANGEBYSORT key (時間戳開始點 時間戳結束點 0,1
# score 開始點, score 結束點, 也就是列出 時間戳開始點 <= 時間戳 < 時間戳結束點 的資料
# 如果開始點或者結束點前面有 ( 這個符号表示 大于等于或者小于等于           

1.10 Redis 持久化

前面提到,Redis 支援持久化的操作,Redis 的資料全部在記憶體中,如果突然挂機,如果不持久化寫入磁盤就會造成資料的全部丢失。Redis 的持久化機制有兩種:一種是 RDB 快照,一種是 AOF 日志。

通常情況下,Redis 主節點不進行持久化操作,持久化操作一般在從節點進行的。

1.10.1 RDB 快照原理:全量持久化

RDB 會将記憶體的資料全量的寫入到磁盤,這也是為什麼叫“快照”的原因。Redis 使用多程序的 COW(Copy On Write)實作記憶體資料的快照持久化。資料恢複比較快。

RDB 的缺點的:會丢失很多的資料。看 COW 原理了解。

什麼是 COW? 參考 Copy On Write機制了解一下

https://cloud.tencent.com/developer/article/1369027

Redis 在持久化的時候,會将目前主程序調用 glibc 的 fork 産生一個子程序。fork 出來的子程序和父程序共享相同的記憶體頁,隻有在資料更改的時候才會分離給子程序配置設定新的實體記憶體。在 Redis 中,子程序不會對記憶體中的資料進行修改。當父程序對其中一個 page 修改的時候,會使用作業系統的 COW 機制,将記憶體分離出來。

父子程序虛拟位址不同,如果記憶體頁資料在 fork 程序後沒有發生改變,則實際實體記憶體位址相同。

Redis 基礎

Redis 主程序對紅色的 page 進行修改,COW 會将父子程序共享的紅色 page 在修改前分離。子程序還是會使用未修改的記憶體頁。這也就是說,Redis 的記憶體在子程序産生的一瞬間瞬間凝固了,相當于給記憶體照相(快照)了。

因為在 Redis 中,父程序(主程序)負責響應請求,也就是會修改記憶體中的資料。而子程序專門負責周遊記憶體,進行系列化寫入磁盤。

主程序修改記憶體頁,會拷貝未修改的記憶體頁給子程序配置設定新的實體記憶體。

Redis 基礎

1.10.2 AOF 日志

AOF 日志,也就是記錄對 Redis 記憶體資料進行修改的指令,當 Redis 當機重新開機後,會根據日志記錄重放一遍(重新執行一遍)。這樣子就可以恢複到當機前的資料了。

Redis 收到用戶端修改指令後,進行參數檢驗,邏輯處理,如果沒有執行成功,就會記錄一條資料。這跟其他的 HBASE 等不同的是,Redis 是先執行再記錄日志的。

AOF 日志的缺點是:如果 AOF 日志很大很大重新執行一遍 AOF 日志很慢很慢。

AOF 瘦身: 原理就是 fork 一個子程序,對目前的記憶體資料周遊,生成新的 AOF 日志,然後跟 fork 後的增量資料合并産生的 AOF。因為 Redis 經常 set 、del,有些已經删除了的資料沒必要還記錄着。

AOF 會丢失資料嗎? 其實 AOF 為了快速寫入,還是會先寫入到記憶體中,是以沒有 fsync 刷寫到磁盤的資料還是會丢失的。不調用 fsync ,交給作業系統完成寫入磁盤的操作性能比較高,但資料丢失的更多。一條日志調用一次 fync,則性能比較低。在生産中一般配置 1s fsync 刷寫磁盤,也就是說最多丢失 1s 的資料。

Kafka 預設是将刷寫磁盤的操作交給作業系統的。是以 Kafka 比較适合允許丢失少量資料的大資料應用。

1.10.3 Redis 4 開始的混合持久化

Redis 重新開機後,會加載 RDB,然後在重放 AOF,這樣子資料丢失的可能比較小,重新開機的效率也比較高。

1.11 Redis Pipeline 管道

管道是加速 Redis 的存取效率,減少網絡 IO 次數,減少 IO 時間。也就是将多個讀取、寫入的操作指令封裝成一個網絡請求。

Kafka 用戶端,将發往同一個 Broker 的消息封裝成 batchs,然後再發送,提高吞吐量。

如何測試 Redis 的 QPS,redis-benchmark:

> redis-benchmark -t set -p 3 -q
# -t set 對 set 指令進行壓測
# -p 3 管道的指令數量,不使用表示 一個指令一個請求
# -q 強制退出 redis。僅顯示 query/sec 值           

1.12 Redis 同步機制

Redis 支援主從同步和從從同步:

Redis 基礎

1.12.1 快照同步

第一次同步或者新節點加入的時候使用快照同步。首先在主節點進行 bgsave 産生 RDB 快照,然後發送給要同步的從節點。

從節點接收 RDB 後會清空目前從節點的資料,全量加載 RDB 快照。對于快照以後的資料采用增量同步的方式。

1.12.2 增量同步

Redis 增量同步的是指令流,主節點會将會改變記憶體資料的指令記錄到本地的環形數組 buffer,然後異步将 buffer 發送到從節點,從節點一邊記錄同步到那裡一邊回報給主節點同步到哪裡了(偏移量)。

環形數組 buffer,循環寫入,寫滿後會覆寫前面寫的内容。如果網絡不好,從節點短時間無法和主節點同步。恢複網絡後,主節點環形 buffer 那些還沒有被同步的資料可能被後續的指令覆寫了。那麼就會采用快照同步的方式。

快照同步死循環:主節點 bgsave 後的增量資料的操作指令流,又發生覆寫了前面還沒有同步的指令。從節點又得重新采用快照同步方式。然後死循環了。是以需要配置一個合适的 buffer 大小。

1.13 Redis 叢集高可用

Redis Sentinel :哨兵,master 當機後,将 slave 提升為 master 繼續提供服務。資料丢失:因為 buffer 增量同步的方式是異步的,可能會丢失部分資料。高可用

Redis Cluster:去中心化,多個節點負責叢集的一部分資料,多個節點也是對等的。預設分成 16384 個槽位,每個節點負責部分資料。擴充性。

Redis 基礎

參考資料:

Redis基礎:

https://mp.weixin.qq.com/s/aOiadiWG2nNaZowmoDQPMQ

錢文品《Redis 深度曆險》