日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

备战面试日记(6.1) - (缓存相关.Redis全知识点)

發布時間:2024/3/24 数据库 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 备战面试日记(6.1) - (缓存相关.Redis全知识点) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本人本科畢業,21屆畢業生,一年工作經驗,簡歷專業技能如下,現根據簡歷,并根據所學知識復習準備面試。

記錄日期:2022.1.15

大部分知識點只做大致介紹,具體內容根據推薦博文鏈接進行詳細復習。

文章目錄

  • Redis
    • 數據結構與對象
      • 數據類型分類(對象)
        • 數據類型概述
        • 編碼和底層實現
      • 數據結構
        • SDS字符串
          • SDS定義
          • SDS 與 C字符串的區別
            • 獲取字符串長度
            • 緩沖區溢出
            • 內存重分配次數
            • 二進制安全
            • 兼容< string.h >庫的函數
        • 鏈表
          • 鏈表定義
          • 特性總結
        • 字典
          • 字典定義
          • 哈希沖突
            • 哈希算法
            • 解決哈希沖突
          • rehash
            • rehash概述
            • rehash條件
            • 漸進式hash過程
            • 漸進式hash執行期間進行哈希表操作
            • 漸進式hash的缺點
        • 跳躍表
          • 為什么redis選擇了跳躍表而不是紅黑樹?
        • 整數集合
          • 整數集合定義
          • 整數集合升級
            • 升級的好處
          • 整數集合降級
        • 壓縮列表
          • 壓縮列表定義
          • 列表節點構成
            • previous_entry_length
            • encoding
            • content
          • 連鎖更新
      • 編碼轉換時機
        • 字符串
          • int
          • raw
          • embstr
          • 為什么raw和embstr的臨界值是44字節?
        • 列表
          • ziplist
          • linkedlist
        • 哈希
          • ziplist
          • hashtable
        • 集合
          • intset
          • hashtable
        • 有序集合
          • ziplist
          • skiplist
    • 持久化
      • RDB
        • 觸發方式
          • 手動觸發
          • 自動觸發
        • RDB優缺點
          • 優點
          • 缺點
      • AOF
        • 執行流程
        • 觸發方式
          • 手動觸發
          • 自動觸發
        • AOF文件同步策略
        • AOF持久化配置
    • 文件事件處理器
      • 組成部分
      • 處理機制
      • 拓展
    • 內存淘汰機制
      • 內存淘汰策略
        • 策略介紹
          • noeviction
          • volatile-ttl、volatile-random、volatile-lru、volatile-lfu
          • allkeys-random、allkeys-lru、allkeys-lfu
        • LRU & LFU算法
          • LRU
            • LRU 篩選邏輯
            • Redis 對 LRU 的實現
          • LFU
            • LFU 篩選邏輯
            • LFU 的具體實現
            • Redis 對 LFU 的實現
            • LFU 中的 counter 值的衰減機制
        • 使用總結
    • 事務
      • 概念
      • 事務階段
      • 事務錯誤處理
      • Watch 監控
        • 引入
        • watch 命令
        • unwatch 命令
      • 總結說明
    • Redis集群
      • 主從復制
        • 主從復制架構
        • 開啟主從復制方式
          • 命令
          • 配置
          • 啟動命令
        • 復制的實現【重點】
          • 1. 設置主服務器的地址和端口
          • 2. 建立套接字連接
          • 3. 發送 PING 命令
          • 4. 身份驗證
          • 5. 發送端口信息
          • 6. 同步
          • 7. 命令傳播
        • 主從復制優缺點
          • 優點
          • 缺點
        • 總結
      • 哨兵模式
        • 哨兵模式架構
        • 哨兵進程
          • 哨兵進程的作用
        • 哨兵(Sentinel) 和 一般Redis 的區別?
        • 哨兵的工作方式
          • 創建連接
          • 獲取主服務器信息
          • 獲取從服務器信息
          • 向主服務器和從服務器發送信息
          • 接收來自主服務器和從服務器的頻道信息
        • 故障檢測
          • 檢測主觀下線
          • 檢測客觀下線
          • 選舉領頭 Sentinel
          • 故障遷移
            • 選出新的主服務器
            • 修改從服務器的復制目標
            • 將舊主服務器變為從服務器
      • 集群模式
        • 集群模式架構
        • 集群數據結構
        • 集群連接方式
        • 分布式尋址算法【引入】
          • hash 算法
          • 一致性 hash 算法
            • hash 環數據傾斜 & 虛擬節點
          • hash slot 算法
          • 一致性 hash 算法 和 hash slot 算法的區別?
            • 定位規則區別
            • 應對熱點緩存區別
            • 擴容和縮容區別
        • 集群的槽指派
          • 指派節點槽信息
            • CLUSTER ADDSLOTS 的命令實現
          • 傳播節點槽信息
          • 記錄集群所有槽的指派信息
            • 使用 `clusterState.slots` 和使用 `clusterNode.slots` 保存指派信息相比的好處?
        • 集群執行命令
          • MOVED 錯誤
        • 節點數據庫的實現
        • 重新分片(比如在線擴容)
          • ASK 錯誤 - (保證集群在線擴容的安全性)
          • CLUSTER SETSLOT IMPORTING 命令的實現
          • CLUSTER SETSLOT MIGRATING 命令的實現
          • ASKING 命令
        • 復制和故障轉移
          • 設置從節點方式
          • 故障檢測
          • 故障轉移
            • 選舉新的主節點過程
    • Redis應用
      • Redis 分布式鎖
        • 引入
          • 為什么需要分布式鎖?
          • 什么時候用分布式鎖?
          • 分布式鎖需要哪些特性呢?
        • 加鎖
        • 解鎖
        • 續鎖
          • 守護線程“續命”存在的問題
          • RedLock
            • RedLock 算法
            • 失敗重試
            • RedLock 的問題

Redis

書籍推薦:《Redis的設計與實現》

博客面試文章推薦:全網最硬核 Redis 高頻面試題解析(2021年最新版)

Redis這篇主要要講解的內容包括:數據結構、redis持久化(aof、rdb)、文件事務處理器、redis內存淘汰機制、事務、redis集群(一致性hash等...)、redis分布式鎖都放在Redis的文章里說明。

還有一部分緩存問題,比如緩存設計以及緩存數據一致性、解決方案-緩存雪崩緩存穿透緩存擊穿等另起一篇寫。

數據結構與對象

數據類型分類(對象)

數據類型概述

Redis主要有5種數據類型,包括String,List,Set,Zset,Hash,滿足大部分的使用要求。

數據類型可以存儲的值操作應用場景
STRING字符串、整數或者浮點數對整個字符串或者字符串的其中一部分執行操作;對整數和浮點數執行自增或者自減操作。做簡單的鍵值對緩存
LIST列表從兩端壓入或者彈出元素;對單個或者多個元素進行修剪;只保留一個范圍內的元素存儲一些列表型的數據結構,類似粉絲列表、文章的評論列表之類的數據
SET無序集合添加、獲取、移除單個元素;檢查一個元素是否存在于集合中;計算交集、并集、差集;從集合里面隨機獲取元素交集、并集、差集的操作,比如交集,可以把兩個人的粉絲列表整一個交集
HASH包含鍵值對的無序散列表添加、獲取、移除單個鍵值對;獲取所有鍵值對;檢查某個鍵是否存在結構化的數據,比如一個對象
ZSET有序集合添加、獲取、刪除元素;根據分值范圍或者成員來獲取元素;計算一個鍵的排名去重但可以排序,如獲取排名前幾名的用戶

另外還有高級的4種數據類型:

  • HyperLogLog:通常用于基數統計。使用少量固定大小的內存,來統計集合中唯一元素的數量。統計結果不是精確值,而是一個帶有0.81%標準差(standard error)的近似值。所以,HyperLogLog適用于一些對于統計結果精確度要求不是特別高的場景,例如網站的UV統計。
  • Geo:redis 3.2 版本的新特性。可以將用戶給定的地理位置信息儲存起來, 并對這些信息進行操作:獲取2個位置的距離、根據給定地理位置坐標獲取指定范圍內的地理位置集合。
  • Bitmap:位圖。
  • Stream:主要用于消息隊列,類似于 kafka,可以認為是 pub/sub 的改進版。提供了消息的持久化和主備復制功能,可以讓任何客戶端訪問任何時刻的數據,并且能記住每一個客戶端的訪問位置,還能保證消息不丟失。

編碼和底層實現

主要是講述上述五種基本類型的底層編碼實現:

類型編碼對象
REDIS_STRINGREDIS_ENCODING_INT使用整數值來實現的字符串對象
REDIS_STRINGREDIS_ENCODING_EMBSTR使用embstr編碼的簡單動態字符串實現的字符串對象
REDIS_STRINGREDIS_ENCODING_RAW使用簡單動態字符串實現的字符串對象
REDIS_LISTREDIS_ENCODING_ZIPLIST使用壓縮列表實現的列表對象
REDIS_LISTREDIS_ENCODING_LINKEDLIST使用雙端鏈表實現的列表對象
REDIS_HASHREDIS_ENCODING_ZIPLIST使用壓縮列表實現的哈希對象
REDIS_HASHREDIS_ENCODING_HT使用字典實現的哈希對象
REDIS_SETREDIS_ENCODING_INTSET使用整數集合實現的集合對象
REDIS_SETREDIS_ENCODING_HT使用字典實現的集合對象
REDIS_ZSETREDIS_ENCODING_ZIPLIST使用壓縮列表實現的有序集合對象
REDIS_ZSETREDIS_ENCODING_SKIPLIST使用跳躍表字典實現的有序集合對象

參考《Redis設計與實現》第一部分 數據結構與對象 的 第八章 對象,p63。

通過上面的整理我們就可以知道他們的具體編碼實現了,整理如下:

  • String:SDS
  • list:壓縮列表、雙向鏈表。
  • hash:壓縮列表、字典。
  • set:整數集合、字典。
  • zset:壓縮列表、跳表。

在Redis中我們可以通過 OBJECT ENCODING命令來查看一個數據庫鍵的值對象的編碼:

redis> SET msg "hello world" OK redis> OBJECT ENCODING msg "embstr"

關于他們具體在什么時候使用什么編碼格式,我們在下文詳細說明!

數據結構

主要說明七種對象:簡單動態字符串、鏈表、字典、跳躍表、整數集合、壓縮列表。

SDS字符串

簡單動態字符串(SDS),用作Redis的默認字符串表示。

SDS定義

每個 sds.h/sdshdr 結構標識一個SDS值:

struct sdshdr {int len; // 記錄buf數組中已使用的字節數量,等于SDS所保存字符串的長度int free; // 記錄buf數組中未使用的字節數量char buf[]; // 字節數組,用于保存字符串 }

tip:buf數組最后一個字節會用來保存’/0’,這也是遵循C字符串以空字符結尾的慣例,但是這個字符不會被計算在len長度中。

遵循的好處就是它可以直接重用一部分C字符串函數庫里面的函數。

SDS 與 C字符串的區別

如果一張表來說明,即:

C字符串SDS
獲取字符串長度的復雜度為O(N)獲取字符串長度的復雜度為O(1)
API是不安全的,可能會造成緩沖區溢出API是安全的,不會造成緩沖區溢出
修改字符串長度N次必然需要執行N次內存重分配修改字符串長度N次最多需要執行N次內存重分配
只能保存文本數據可以保存文本數據或者二進制數據
可以使用所有的<string.h>庫中的函數可以使用一部分<string.h>庫中的函數

那我們根據這五點來說明,這五大區別的產生原因:

獲取字符串長度

原因如下:

  • C字符串必須遍歷字符串直到碰到結尾的空字符為止復雜度為O(N)
  • SDS字符串在len屬性中記錄了SDS本身的長度復雜度為O(1)

其中SDS長度的設置與更新是由SDS的API執行時自動完成的。

緩沖區溢出

因為C字符串沒有記錄字符串長度,所以如果使用如下方法:

char *strcat(char *dest, const char *src);

當開發者已經為 dest 字符串分配了一定的內存,此時如果 src 字符串中內容拼接進去后的內存大于分配的內存,則會造成緩沖區溢出。

那么SDS字符串是如何解決的呢?

當 SDS API 需要對 SDS 進行修改時,API 會先檢查 SDS 的空間是否滿足所需的要求,如果不滿足的話,API 會自動將 SDS 的空間擴展至執行修改所需的大小,然后才執行實際的修改操作,所以使用 SDS 既不需要后動修改 SDS 的空間大小,也不會出現C字符串中的緩沖區溢出問題。

內存重分配次數

因為C字符串的底層實現總是 N + 1 個字符串長度的數組。所以每次執行 增長字符串 或是 縮短字符串時,都要先通過重分配擴展底層數組的空間大小 或是 釋放字符串不再使用的空間,來防止緩沖區溢出 或者 內存泄漏。

那么SDS字符串是如何解決的呢?

SDS中使用free屬性記錄未使用空間的字節數量。

通過未使用的空間,SDS 實現了 空間預分配惰性空間釋放 兩種優化策略。

空間預分配的操作是:當 SDS 的 API 對一個 SDS 進行修改,并且需要對 SDS 進行空間擴展的時候,程序不僅會為 SDS 分配修改所必須要的空間,還會為 SDS 分配額外的未使用空間。

這里存在兩種修改情況:

  • 對SDS修改后,SDS長度(即len值)< 1MB:這是 len值 會和 free值 相同。此時 buf數組 實際長度是 len + free + 1。
  • 對SDS修改后,SDS長度(即len值)> 1MB:會多分配 1MB 未使用空間,比如 len值 為30MB時,此時 buf數組 實際長度是 30MB + 1MB + 1byte。
  • 惰性空間釋放的操作是:當 SDS 的 API 對 一個 SDS 進行修改,并且需要對 SDS 所保存的字符串進行縮短時,程序并不立即使用內存重分配來回收縮短后多出來的字節,而是使用 free屬性 將這些字節的數量記錄起來,并等待將來使用。

    當然,如果需要真正地釋放 SDS 的未使用空間,會有 API 去實現,這里不說明。

    二進制安全

    C字符串的字符必須符合某種編碼(比如ASCII),并且除了末尾空字符外,不能包含任何空字符,否則會被程序誤認為是末尾,這使得C字符串只能保存文本數據,而不能保存二進制數據。

    那么SDS字符串是如何解決的呢?

    SDS 的 API 都是二進制安全的,所有的 SDS API 都會以處理二進制的方式來處理 SDS 存放的 buf數組 里的數據。

    所以SDS 的 buf屬性被稱為字節數組,就是因為它是用來保存一系列二進制數據。

    兼容< string.h >庫的函數

    上面說過了,SDS 也遵循C字符串以空字符結尾的慣例,就是為了能讓它使用部分<string.h>庫的函數。

    鏈表

    鏈表定義

    每個鏈表節點使用一個 adlist.h/listNode 結構來表示:

    typedef struct listNode {struct listNode *prev; // 前置指針struct listNode *next; // 后置指針void *value; // 節點的值 }

    說明該鏈表是一個雙向鏈表。

    當我們使用多個 listNode 組成鏈表,就會直接使用 adlist.h/list 來持有該鏈表進行操作:

    typedef struct list {listNode *head; // 表頭節點listNode *tail; // 表尾節點unsigned long len; // 鏈表所包含的節點數量void *(*dup) (void *ptr); // 節點值復制函數void *(*free) (void *ptr); // 節點值釋放函數int (*match) (void *ptr, void *key); // 節點值對比函數 }
    特性總結
    • 雙端:節點有 prev 和 next 指針,復雜度為O(1)。
    • 無環:對鏈表的訪問都是以NULL為終點。
    • 帶頭尾指針:list 中有head 和 tail 指針,復雜度為O(1)。
    • 帶鏈表長度計數屬性:len屬性保存節點數,復雜度為O(1)。
    • 多態:使用 void*指針保存節點值,可以保存不同類型的值。

    字典

    即數組 + 鏈表實現。

    字典定義

    Redis 字典所使用的哈希表由 dict.h/dictht 結構定義:

    typedef struct dictht {dictEntry **table; // 哈希表數組unsigned long size; // 哈希表大小unsigned long sizemask; // 哈希表大小掩碼,用于計算索引值,總是等于 size - 1unsigned long used; // 哈希表已有節點數量 }

    哈希表節點使用 dictEntry 結構表示,每個 dictEntry 結構都保存著一個kv對:

    typedef struct dictEntry {void *key; // 鍵union { // 值void *val;uint64_t u64;uint64_t s64;}struct dictEntry *next; // 指向下個哈希表節點,形成鏈表 }

    Redis 中的字典由 dict.h/dict 結構表示:

    typedef struct dict {dictType *type; // 類型特定函數void *privdata; // 私有數據dictht ht[2]; // 哈希表int trehashidx; // rehash索引,當rehash不在進行時,值為1 }
    哈希沖突
    哈希算法

    在添加新的鍵值到字典里是,要先進行對key的哈希,根據哈希值計算出索引值,根據索引將新的kv對放到哈希表數組的指定索引上。

    index = hash&dict -> ht[0].sizemask

    Redis 使用 MurmurHash 算法。

    解決哈希沖突

    Redis 的哈希表使用鏈地址法解決哈希沖突,并且使用的是頭插法

    rehash

    hash 對象在擴容時使用了一種叫 “漸進式 rehash” 的方式。

    rehash概述

    擴展收縮哈希表的工作都是通過執行 rehash 來完成的。

    reash的步驟如下:

  • 計算新表(ht[1])的空間大小,取決于舊表(ht[0])當前包含的鍵值以及數量。

  • 如果是擴展操作,那么新表(ht[1])的大小為第一個大于等于 ht[0].used * 2 的 2^N。
  • 如果是收縮操作,那么新表(ht[1])的大小為第一個大于等于ht[0].used 的 2^N。
  • 將保存在舊表(ht[0])的所有鍵值rehash到新表(ht[1])上。

  • 當舊表(ht[0])全部遷移完成后,釋放舊表(ht[0]),將新表設置為 ht[0] 并在 ht[1]重新創建一張空白哈希表。

  • 這兩個哈希表的套路是不是有點像jvm運行時數據區的年輕代的幸存者區?可以引申一下。

    rehash條件

    當下面兩個條件任意一個被滿足時,程序就會自動開始對哈希表進行擴展操作:

  • 當前服務器沒有在執行 BGSAVE 命令或 BGREWRITEAOF 指令,并且哈希表的負載因子大于等于1。
  • 當前服務器正在執行 BGSAVE 命令或 BGREWRITEAOF 指令,并且哈希表的負載因子大于等于5。【5是因為已保存節點數量包括沖突節點】
  • 為什么這兩個命令的是否正在執行,和服務器執行擴展操作的負載因子并不相同?

    答:是因為在執行BGSAVE命令或者BGREWRITEAOF命令的過程中,Redis需要fork子線程,而大多數os都采用與時復制技術來優化子進程的使用效率,所以子進程存在的期間,服務器會提高執行擴展操作所需的負載因子,從而盡可能地避免在子進程存在期間進行哈希擴容,可以避免不必要的內存寫入操作,節約內存。

    與時復制:copy-on-write,即不用復制寫入直接引用父進程的物理過程。

    BGSAVE命令:fork子進程去完成備份持久化。(區別于SAVE命令,阻塞線程去完成備份持久化)

    BGREWRITEAOF命令:異步執行AOF重寫,優化原文件大小(該命令執行失敗不會丟失數據,成功才會真正修改數據,2.4以后手動觸發該命令)

    漸進式hash過程

    漸進式rehash的詳細步驟:

  • 為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表。
  • 在字典中維持一個索引計數器變量rehashidx,并將它的值設為0,表示rehash工作正式開始。
  • 在rehash進行過程中,每次對字典進行添加、刪除、查找、更新操作時,除了執行指定操作以外,還會順帶將ht[0]在rehashidx索引上的所有鍵值對rehash到ht[1]上,當rehash工作完成時,rehashidx屬性值加一。
  • 隨著字典操作的不斷執行,最終在某一個時間點上,ht[0]的所有鍵值對都會被rehash到ht[1]上,這是將rehashidx的值設為-1,表示rehash操作已完成。
  • 漸進式hash采取 分而治之 的思想,將rehash鍵值對所需的計算工作均攤到字典的每個添加、刪除、查找、更新操作上,避免集中式hash。

    漸進式hash執行期間進行哈希表操作
  • 進行刪除、查找、更新操作時,都會在兩個哈希表上進行。比如說查找操作,現在ht[0]上查找,如果ht[0]上沒有就去ht[1]上查找。
  • 進行添加操作時,新的鍵值對直接保存在ht[1]中,而ht[0]不進行操作,這樣保證ht[0]只減不增。
  • 漸進式hash的缺點
  • 擴容期開始時,會先給 ht[1] 申請空間,所以在整個擴容期間,會同時存在 ht[0] 和 ht[1],會占用額外的空間。

  • 擴容期間同時存在 ht[0] 和 ht[1],查找、刪除、更新等操作有概率需要操作兩張表,耗時會增加。

  • redis 在內存使用接近 maxmemory 并且有設置驅逐策略的情況下,出現 rehash 會使得內存占用超過 maxmemory,觸發驅逐淘汰操作,導致 master/slave 均有有大量的 key 被驅逐淘汰,從而出現 master/slave 主從不一致。

  • 跳躍表

    可以把他理解為一個可以二分查找的鏈表

    它在Redis中只用到過兩處:一是有序集合zset;二是集群節點的內部數據結構。

    這塊的實現就不整理,看博客 或者 看書吧,《Redis設計與實現》p38。

    參考博客鏈接一:面試準備 – Redis 跳躍表

    參考博客鏈接二:Redis中的跳躍表

    參考博客鏈接三:跳躍表以及跳躍表在redis中的實現

    為什么redis選擇了跳躍表而不是紅黑樹?
    • 在做范圍查找的時候,平衡樹比 skiplist 操作要復雜。
      • 在平衡樹上,我們找到指定范圍的小值之后,還需要以中序遍歷的順序繼續尋找其它不超過大值的節點。如果不對平衡樹進行一定的改造,這里的中序遍歷并不容易實現。
      • 而在 skiplist 上進行范圍查找就非常簡單,只需要在找到小值之后,對第1層鏈表進行若干步的遍歷就可以實現。
    • 平衡樹的插入和刪除操作可能引發子樹的調整,邏輯復雜,而 skiplist 的插入和刪除只需要修改相鄰節點的指針,操作簡單又快速。
    • 從內存占用上來說,skiplist比平衡樹更靈活一些。
      • 平衡樹每個節點包含2個指針(分別指向左右子樹)。
      • skiplist 每個節點包含的指針數目平均為1/(1-p),具體取決于參數p的大小。如果像Redis里的實現一樣,取p=1/4,那么平均每個節點包含1.33個指針,比平衡樹更有優勢。
    • 查找單個key,skiplist和平衡樹的時間復雜度都為O(log n),大體相當;而哈希表在保持較低的哈希值沖突概率的前提下,查找時間復雜度接近O(1),性能更高一些。所以我們平常使用的各種 Map 或 dictionary 結構,大都是基于哈希表實現的。
    • 從算法實現難度上來比較,skiplist 比平衡樹要簡單得多。

    整數集合

    整數集合定義

    每個 intset.h/intset 結構表示一個整數集合:

    typedef struct intset {uint32_t encoding; // 編碼方式uint32_t length; // 集合包含的元素數量int8_t contents[]; // 保存元素的數組 }

    其中 contents[]就是整數集合的底層實現:整數集合的每個元素都是該數組的一個數組項,各個項在數組中是從小到大有序排列,并且不重復。

    雖然 contents[] 屬性聲明是 int8_t,但是真正類型取決于 encoding。

    整數集合升級

    整數升級,即當我們將一個新元素添加到集合中時,新元素的類型比原集合的類型都要長時,整數集合需要升級,然后才能將新元素添加到集合中。

    具體升級并添加元素的步驟分為三步:

  • 根據新元素的類型,擴展底層數組的空間大小,并為新元素分配空間。
  • 將底層數組現有的所有元素都轉換成與新元素相同的類型,并將類型轉換后的元素放置到正確的位置。該過程中,底層數組的順序不可變。
  • 將新元素加入數組。
  • 該過程的復雜度為 O(N)。

    升級的好處
  • 提升整數集合的靈活性。
  • 盡可能節約內存。
  • 整數集合降級

    整數集合不支持降級操作!

    壓縮列表

    它的存在意義就是為了節約內存

    壓縮列表定義

    壓縮列表就是一個由一系列特殊編碼的連續內存塊組成的順序型數據結構。

    壓縮列表的各個組成部分說明如下表:

    屬性類型長度用途
    zlbytesuint32_t4字節記錄整個壓縮鏈表占用的字節數,在對壓縮列表進行內存重分配,或者計算zlend的位置時使用。
    zltailuint32_t4字節記錄壓縮列表表尾節點距離壓縮列表起始地址有多少個字節:通過這個偏移量,程序無須遍歷整個壓縮列表就可以確定尾節點的地址。
    zllenuint16_t2字節記錄了壓縮列表包含的字節數量,該屬性小于UINT16_MAX(65535)時,該值為壓縮列表包含節點的數量;該屬性等于UINT16_MAX(65535)時,節點的真實數量需要遍歷壓縮列表獲得。
    entryX列表節點不定壓縮列表包含的各個節點,節點的長度由節點保存的內容而定。
    zlenduint8_t1字節特殊值0xFF(十進制255),用于標記壓縮列表的末端。
    列表節點構成

    每個壓縮列表節點可以保存一個字節數組或者一個整數值。其中,字節數組可以是以下三種長度之一:

    • 長度小于等于63(2^6 - 1)字節的字節數組;
    • 長度小于等于16383(2^14 - 1)字節的字節數組;
    • 長度小于等于4294967295(2^32 - 1)字節的字節數組;

    而整數值則可以是以下六種長度的其中一種:

    • 4位長,介于0至12之間的無符號整數;
    • 1字節長的有符號;
    • 3字節長的有符號整數;
    • int16_t類型整數;
    • int32_t類型整數;
    • int64_t類型整數。

    每個壓縮列表節點都由 previous_entry_length、encoding、content三個部分組成:

    previous_entry_length

    節點的 previous_entry_length 屬性以字節為單位,記錄了壓縮列表中前一個節點的長度

    previous_entry_length 屬性的長度可以是1字節 或者 5字節:

    • 如果前一節點的長度小于254字節,那么 previous_entry_length 屬性的長度為1字節:前一節點的長度就保存在這一個字節里面。
    • 如果前一節點的長度大于等于254字節,那么 previous_entry_length 屬性的長度為5字節:其中屬性的第一字節會被設置為0xFE(十進制254),而之后的四個字節則用于保存前一節點的長度。

    它的好處就是,因為節點的 previous_entry_length 屬性記錄了前一個節點的長度,所以程序可以通過指針運算,根據當前節點的起始地址來計算出前一節點的起始地址。

    壓縮列表的從表尾向表頭遍歷操作就是使用這一原理實現的,只要我們擁有一個指向某個節點起始地址的指針,那么通過這個指針以及這個節點的 previous_entry_length 屬性,程序就可以一直向前一個節點回溯,最終到達壓縮列表的表頭節點。

    encoding

    節點的 encoding 屬性記錄了節點的 content 屬性所保存數據的類型以及長度

    • 1字節、2字節或者5字節長,值的最高位為00、01或者10的是字節數組編碼:這種編碼表示節點的 content 屬性保存著字節數組,數組的長度由編碼除去最高兩位之后的其他位記錄;
    • 1字節長,值的最高位以11開頭的是整數編碼:這種編碼表示節點的 content 屬性保存著整數值,整數值的類型和長度由編碼除去最高兩位之后的其他位記錄。
    content

    節點的 content 屬性負責保存節點的值,節點值可以是一個字節數組或者整數值,值的類型和長度由節點的 encoding 屬性決定。

    連鎖更新

    redis中的壓縮列表在插入數據的時候可能存在連鎖擴容的情況。

    在壓縮列表中,節點需要存放上一個節點的長度:當上一個entry節點長度小于254個字節的時候,將會一個字節的大小來存放entry中的數據;但是當上一個entry節點長度大于等于254個字節的時候,就會需要更大的空間來存放數據。

    在壓縮列表中,會把大于等于254字節長度用5個字節來存儲,第一個字節是254,當讀到254的時候,將會確認接下來的4個字節大小將是entry的長度數據。當第一個字節為255的時候,就證明壓縮列表已經到達末端。

    由于表示長度的字節大小不一樣,當新節點的插入可能會導致下一個節點原本存放表示上一節點的長度的空間大小不夠導致需要擴容這一字段。相應的該字段將會由一個字節擴容到五個字節,四個字節的長度變化,當發生變化的節點原本長度在250到253之間的時候,將會導致下一個節點存儲上節點長度的空間發生變化,引起一個連鎖擴容的情況,這一情況將會直到一個不需要擴容的節點為止。

    擴容邏輯代碼如下,可參考:

    while (p[0] != ZIP_END) {zipEntry(p, &cur);rawlen = cur.headersize + cur.len;rawlensize = zipStorePrevEntryLength(NULL,rawlen);/* Abort if there is no next entry. */if (p[rawlen] == ZIP_END) break;zipEntry(p+rawlen, &next);/* Abort when "prevlen" has not changed. */if (next.prevrawlen == rawlen) break;if (next.prevrawlensize < rawlensize) {/* The "prevlen" field of "next" needs more bytes to hold* the raw length of "cur". */offset = p-zl;extra = rawlensize-next.prevrawlensize;zl = ziplistResize(zl,curlen+extra);p = zl+offset;/* Current pointer and offset for next element. */np = p+rawlen;noffset = np-zl;/* Update tail offset when next element is not the tail element. */if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);}/* Move the tail to the back. */memmove(np+rawlensize,np+next.prevrawlensize,curlen-noffset-next.prevrawlensize-1);zipStorePrevEntryLength(np,rawlen);/* Advance the cursor */p += rawlen;curlen += extra;} else {if (next.prevrawlensize > rawlensize) {/* This would result in shrinking, which we want to avoid.* So, set "rawlen" in the available bytes. */zipStorePrevEntryLengthLarge(p+rawlen,rawlen);} else {zipStorePrevEntryLength(p+rawlen,rawlen);}/* Stop here, as the raw length of "next" has not changed. */break;} }

    代碼邏輯是:首先,從新插入的節點的下一個節點開始,如果下一個節點存放上一個字節的空間大小大于或等于當前的節點長度,那么在存放了這一長度數據之后,該次連鎖擴容直接宣告結束。如果下一個節點存放長度的空間不能容納當前節點的長度,那么就會將下一個節點進行擴容,并重新申請內存大小,并復制數據,移動指向尾部節點的指針。最后移動到下一個節點,在下一個循環中判斷是否需要繼續擴容。

    編碼轉換時機

    Redis中的每個對象都由一個 redisObject 結構來表示:

    typedef struct redisObject {unsigned type:4; // 類型unsigned encoding:4; // 編碼void *ptr; // 指向底層實現數據結構的指針 }

    類型包括基本的五種,編碼指對應類型下的不同編碼實現。

    Redis可以根據不同的使用場景,來為一個對象設置不同的編碼,從而優化對象在某一場景下的效率。

    字符串

    字符串的編碼可以是 int 、 raw 或者是 embstr。

    int

    如果一個字符串對象保存的是整數值,并且這個整數值可以用long類型來表示,那么這個字符串對象會將整數值保存在字符串對象結構的 ptr 屬性中(將 void* 轉換成 long),并將字符串對象的編碼設置為int。

    raw

    如果一個字符串對象保存的是一個字符串值,并且長度大于44字節,那么這個字符串對象將使用簡單動態字符串(SDS)來保存,并且編碼設置為 raw。

    embstr

    如果一個字符串對象保存的是一個字符串值,并且長度小于等于44字節,那么同上,但是編碼設置為embstr。

    embstr 是專門用于保存短字符串的優化編碼方式。它和 raw 的區別在于,raw編碼會調用兩次內存分配函數來分別創建 redisObject 和 sdshdr 結構,而embstr 編碼則通過調用一次內存分配函數來分配一塊連續的空間,空間中依次包含 redisObject 和 sdshdr 結構。

    使用 embstr 的好處:

  • 內存分配次數減少一次。
  • 釋放內存時的調用函數次數也少一次。
  • embstr 保存在連續的內存中,它可以更好地利用緩存帶來的優勢。
  • 不過,embstr 編碼沒有任何相應的修改程序,它實際上只是只讀的,當 embstr 編碼的字符串執行修改命令時,總會變成 raw。

    為什么raw和embstr的臨界值是44字節?

    如果看過書的同學有疑問很正常,因為在《Redis的設計與實現》中,它寫的臨界值是39字節,但是實際上經過查找資料,在3.2版本之后就改成了44字節了。主要原因是為了內存優化,具體解釋如下:

    我們知道對于每個 sds 都有一個 sdshdr,里面的 len 和 free 記錄了這個 sds 的長度和空閑空間,但是這樣的處理十分粗糙,使用的 unsigned int 可以表示很大的范圍,但是對于很短的 sds 有很多的空間被浪費了(兩個unsigned int 8個字節)。而這個 commit 則將原來的 sdshdr 改成了 sdshdr16 , sdshdr32 , sdshdr64 ,里面的 unsigned int 變成了 uint8_t ,uint16_t…(還加了一個char flags)這樣更加優化小 sds 的內存使用。

    本身就是針對短字符串的 embstr 自然會使用最小的 sdshdr8 ,而 sdshdr8 與之前的 sdshdr 相比正好減少了5個字節(sdsdr8 = uint8_t * 2 + char = 1*2+1 = 3, sdshdr = unsigned int * 2 = 4 * 2 = 8),所以其能容納的字符串長度增加了5個字節變成了44。

    列表

    列表的編碼可以是 ziplist 或者 linkedlist。(壓縮列表 或者 雙向鏈表)

    ziplist

    如果列表對象保存的所有字符串元素的長度都小于64字節,并且列表對象保存的元素數量小于512個時,編碼為 ziplist。

    linkedlist

    上面兩個條件,只要一個不滿足,就采取 linkedlist 編碼。

    哈希

    哈希對象的編碼可以是 ziplist 或者 hashtable。(壓縮列表 或者 字典)

    ziplist

    如果哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于64字節,并且哈希對象保存的鍵值對數量小于512個時,編碼為 ziplist。

    hashtable

    上面兩個條件,只要一個不滿足,就采取 hashtable 編碼。

    集合

    集合對象的編碼可以是 intset 或者 hashtable。

    intset

    如果集合對象保存的所有元素都是整數值,并且哈希對象保存的元素數量小于512個時,編碼為 intset。

    hashtable

    上面兩個條件,只要一個不滿足,就采取 hashtable 編碼。

    有序集合

    有序集合的編碼可以是 ziplist 或者 skiplist。

    ziplist

    如果有序集合對象保存的所有元素成員的長度都小于64字節,并且有序集合對象保存的元素數量小于128個時,編碼為 ziplist。

    skiplist

    上面兩個條件,只要一個不滿足,就采取 skiplist 編碼。

    持久化

    詳細了解參考文章:Redis的兩種持久化RDB和AOF(超詳細)

    Redis對數據的操作都是基于內存的,當遇到了進程退出、服務器宕機等意外情況,如果沒有持久化機制,那么Redis中的數據將會丟失無法恢復。有了持久化機制,Redis在下次重啟時可以利用之前持久化的文件進行數據恢復。

    Redis支持的兩種持久化機制:

    • RDB:把當前數據生成快照保存在硬盤上。
    • AOF:記錄每次對數據的操作到硬盤上。
    • 混合持久化:在 redis 4 引入,RDB + AOF 混合使用的方式,RDB 持久化全量數據,AOF 持久化增量數據。

    RDB

    RDB(Redis DataBase)持久化是把當前Redis中全部數據生成快照保存在硬盤上。RDB持久化可以手動觸發,也可以自動觸發。

    觸發方式

    手動觸發

    save 和 bgsave 命令都可以手動觸發RDB持久化。

    • 執行save命令會手動觸發RDB持久化,但是save命令會阻塞Redis服務,直到RDB持久化完成。當Redis服務儲存大量數據時,會造成較長時間的阻塞,不建議使用。
    • 執行bgsave命令也會手動觸發RDB持久化,和save命令不同是:Redis服務一般不會阻塞。Redis進程會執行fork操作創建子進程RDB持久化由子進程負責,不會阻塞Redis服務進程。Redis服務的阻塞只發生在fork階段,一般情況時間很短。
    • 執行 bgsave 命令,Redis進程先判斷當前是否存在正在執行的RDB或AOF子線程,如果存在就是直接結束。
    • Redis進程執行 fork 操作創建子進程,在fork操作的過程中Redis進程會被阻塞。
    • Redis進程 fork 完成后, bgsave 命令就結束了,自此Redis進程不會被阻塞,可以響應其他命令。
    • 子進程根據Redis進程的內存生成快照文件,并替換原有的RDB文件。
    • 子進程通過信號量通知Redis進程已完成。

    簡單說明,save命令會全程阻塞,bgsave只在創建子線程時會阻塞。

    自動觸發

    在以下幾種場景下,會自動觸發RDB持久化:

  • 在配置文件中設置了 save 的相關配置,如sava m n,它表示在 m 秒內數據被修改過 n 次時,自動觸發 bgsave 操作。
  • 當從節點做全量復制時,主節點會自動執行 bgsave 操作,并且把生成的RDB文件發送給從節點。
  • 執行 debug reload 命令時,也會自動觸發 bgsave 操作。
  • 執行 shutdown 命令時,如果沒有開啟AOF持久化也會自動觸發 bgsave 操作。
  • RDB優缺點

    優點
  • RDB文件是一個緊湊的二進制壓縮文件,是Redis在某個時間點的全部數據快照。所以使用RDB恢復數據的速度遠遠比AOF的快,非常適合備份、全量復制、災難恢復等場景。
  • 缺點
  • 如果數據集非常巨大,并且 CPU 時間非常緊張的話,那么這種停止時間甚至可能會長達整整一秒。
  • 每次進行bgsave操作都要執行fork操作創建子經常,屬于重量級操作,頻繁執行成本過高,所以無法做到實時持久化,或者秒級持久化。
  • 由于Redis版本的不斷迭代,存在不同格式的RDB版本,有可能出現低版本的RDB格式無法兼容高版本RDB文件的問題。
  • AOF

    執行流程

  • 命令追加(append):所有寫命令都會被追加到AOF緩存區(aof_buf)中。
  • 文件同步(sync):根據不同策略將AOF緩存區同步到AOF文件中。
  • 文件重寫(rewrite):定期對AOF文件進行重寫,以達到壓縮的目的。
  • 數據加載(load):當需要恢復數據時,重新執行AOF文件中的命令。
  • 觸發方式

    手動觸發

    使用 bgrewriteaof 命令。

    自動觸發

    根據 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 配置確定自動觸發的時機。

    • auto-aof-rewrite-min-size 表示運行AOF重寫時文件大小的最小值,默認為64MB。
    • auto-aof-rewrite-percentage 表示當前AOF文件大小和上一次重寫后AOF文件大小的比值的最小值,默認為100。

    只用前兩者同時超過閾值時才會自動觸發文件重寫。

    AOF文件同步策略

    AOF持久化流程中的文件同步有以下幾個策略:

    • always:每次寫入緩存區都要同步到AOF文件中,硬盤的操作比較慢,限制了Redis高并發,不建議配置。
    • no:每次寫入緩存區后不進行同步,同步到AOF文件的操作由操作系統負責,每次同步AOF文件的周期不可控,而且增大了每次同步的硬盤的數據量。
    • eversec:每次寫入緩存區后,由專門的線程每秒鐘同步一次,做到了兼顧性能和數據安全。是建議的同步策略,也是默認的策略。

    AOF持久化配置

    # appendonly改為yes,開啟AOF appendonly yes # AOF文件的名字 appendfilename "appendonly.aof" # AOF文件的寫入方式 # everysec 每個一秒將緩存區內容寫入文件 默認開啟的寫入方式 appendfsync everysec # 運行AOF重寫時AOF文件大小的增長率的最小值 auto-aof-rewrite-percentage 100 # 運行AOF重寫時文件大小的最小值 auto-aof-rewrite-min-size 64mb

    文件事件處理器

    推薦博客文章:Redis全面解析一:redis是單線程結構為何還可以支持高并發

    我們經常說Redis是單線程的,但是為什么這么說呢?

    因為 Redis 內部用的是基于 Reactor 模式開發的文件事件處理器,文件事件處理器是以單線程方式運行的,所以redis才叫單線程模型。

    組成部分

    基于 Reactor 模式設計的四個組成部分的結構如下所示:

    它們分別是:

    • 套接字
    • IO多路復用程序
    • 文件事件分派器
    • 事件處理器

    處理機制

    文件事件處理器大致可分為三個處理流程:

  • 每一個套接字準備好執行連接應答、寫入、讀取、關閉等操作時,就會產生一個文件事件。一個服務器會連接多個套接字,多個文件事件并發的出現。
  • I/O多路復用程序負責監聽多個套接字,并向文件事件分派器傳送那些產生的套接字,I/O多路復用程序會將所有產生事件的套接字都放到一個隊列里面,然后通過這個隊列,以有序、同步、每次一個套接字的方式向文件事件分派器傳送套接字。當上一個套接字處理完畢,接受下一個套接字。
  • 文件事件分派器接收I/O多路復用程序傳來的套接字,并根據套接字產生的事件的類型,調用相應的事件處理器。(執行不同任務的套接字關聯不同的事件處理器)。
  • 拓展

    關于Redis6.0的多線程升級博客參考鏈接:Redis6 新特性多線程解析

    內存淘汰機制

    Redis 緩存使用內存保存數據,避免了系統直接從后臺數據庫讀取數據,提高了響應速度。由于緩存容量有限,當緩存容量到達上限,就需要刪除部分數據挪出空間,這樣新數據才可以添加進來。Redis 定義了「淘汰機制」用來解決內存被寫滿的問題。

    緩存淘汰機制,也叫緩存替換機制,它需要解決兩個問題:

    • 決定淘汰哪些數據。
    • 如何處理那些被淘汰的數據。

    內存淘汰策略

    截至在 4.0 之后,Redis定義了「8種內存淘汰策略」用來處理 redis 內存滿的情況:

    • noeviction:不會淘汰任何數據,當使用的內存空間超過 maxmemory 值時,返回錯誤。
    • volatile-ttl:篩選設置了過期時間的鍵值對,越早過期的越先被刪除。
    • volatile-random:篩選設置了過期時間的鍵值對,隨機刪除。
    • volatile-lru:使用 LRU 算法篩選設置了過期時間的鍵值對。
    • volatile-lfu:使用 LFU 算法選擇設置了過期時間的鍵值對。
    • allkeys-random:在所有鍵值對中,隨機選擇并刪除數據。
    • allkeys-lru:使用 LRU 算法在所有數據中進行篩選。
    • allkeys-lfu:使用 LFU 算法在所有數據中進行篩選。

    根據它們的名稱和前綴我們就能如下分類:

    • 不淘汰數據:noeviction。
    • 淘汰數據
      • 設置了過期時間的鍵值對中進行淘汰:volatile-ttl、volatile-random、volatile-lru、volatile-lfu。
      • 所有數據進行淘汰:allkeys-random、allkeys-lru、allkeys-lfu。

    策略介紹

    noeviction

    noeviction 策略,也是 Redis 的默認策略,它要求 Redis 在使用的內存空間超過 maxmemory 值時,也不進行數據淘汰。一旦緩存被寫滿了,再有寫請求來的時候,Redis 會直接返回錯誤。

    我們實際項目中,一般不會使用這種策略。因為我們業務數據量通常會超過緩存容量的,而這個策略不淘汰數據,導致有些熱點數據保存不到緩存中,失去了使用緩存的初衷。

    volatile-ttl、volatile-random、volatile-lru、volatile-lfu

    volatile-random、volatile-ttl、volatile-lru、volatile-lfu 這四種淘汰策略。它們淘汰數據的時候,只會篩選設置了過期時間的鍵值對上。

    比如,我們使用 EXPIRE 命令對一批鍵值對設置了過期時間,那么會有兩種情況會對這些數據進行清理:

  • 第一種情況是過期時間到期了,會被刪除。
  • 第二種情況是 Redis 的內存使用量達到了 maxmemory 閾值,Redis 會根據 volatile-random、volatile-ttl、volatile-lru、volatile-lfu 這四種淘汰策略,具體的規則進行淘汰;這也就是說,如果一個鍵值對被刪除策略選中了,即使它的過期時間還沒到,也需要被刪除。
  • 其中 volatile-ttl、volatile-random的篩選規則比較簡單,而volatile-lru、volatile-lfu分別用到了 LRU 和 LFU 算法。

    allkeys-random、allkeys-lru、allkeys-lfu

    allkeys-random,allkeys-lru,allkeys-lfu 這三種策略跟上述四種策略的區別是:淘汰時數據篩選的數據范圍是所有鍵值對。

    其中allkeys-random的篩選規則比較簡單,而allkeys-lru,allkeys-lfu分別用到了LRU 和 LFU 算法。

    LRU & LFU算法

    LRU

    LRU 算法全稱 Least Recently Used,一種常見的頁面置換算法。按照「最近最少使用」的原則來篩選數據,篩選出最不常用的數據,而最近頻繁使用的數據會留在緩存中。

    LRU 篩選邏輯

    RU 會把所有的數據組織成一個鏈表,鏈表的頭和尾分別表示 MRU 端和 LRU 端,分別代表「最近最常使用」的數據和「最近最不常用」的數據。

    每次訪問數據時,都會把剛剛被訪問的數據移到 MRU 端,就可以讓它們盡可能地留在緩存中。

    如果此時有新數據要寫入時,并且沒有多余的緩存空間,那么該鏈表會做兩件事情:

  • 將新數據放到MRU端。
  • 將LRU端的數據刪除。
  • 簡單說明,即它認為剛剛被訪問的數據,肯定還會被再次訪問,所以就把它放在 MRU端;LRU 端的數據被認為是長久不訪問的數據,在緩存滿時,就優先刪除它。

    Redis 對 LRU 的實現

    Redis 3.0 前,隨機選取 N 個淘汰法。

    Redis 默認會記錄每個數據的最近一次訪問的時間戳(由鍵值對數據結構 RedisObject 中的 lru 字段記錄)。

    在 Redis 決定淘汰的數據時,隨機選 N(默認5) 個 key,把空閑時間(idle time)最大的那個 key 移除。這邊的 N 可通過 maxmemory-samples 配置項修改:

    config set maxmemory-samples 100

    當需要再次淘汰數據時,Redis 需要挑選數據進入「第一次淘汰時創建的候選集合」。

    挑選的標準是:能進入候選集合的數據的 lru 字段值必須小于「候選集合中最小的 lru 值」。

    當有新數據進入備選數據集后,如果備選數據集中的數據個數達到了設置的閾值時。Redis 就把備選數據集中 lru 字段值最小的數據淘汰出去

    Redis3.0后,引入了緩沖池(默認容量為16)概念。

    當每一輪移除 key 時,拿到了 N(默認5)個 key 的 idle time,遍歷處理這 N 個 key,如果 key 的 idle time 比 pool 里面的 key 的 idle time 還要大,就把它添加到 pool 里面去。

    當 pool 放滿之后,每次如果有新的 key 需要放入,需要將 pool 中 idle time 最小的一個 key 移除。這樣相當于 pool 里面始終維護著還未被淘汰的 idle time 最大的 16 個 key。

    當我們每輪要淘汰的時候,直接從 pool 里面取出 idle time 最大的 key(只取1個),將之淘汰掉。

    整個流程相當于隨機取 5 個 key 放入 pool,然后淘汰 pool 中空閑時間最大的 key,然后再隨機取 5 個 key放入 pool,繼續淘汰 pool 中空閑時間最大的 key,一直持續下去。

    在進入淘汰前會計算出需要釋放的內存大小,然后就一直循環上述流程,直至釋放足夠的內存。

    LFU

    在一些場景下,有些數據被訪問的次數非常少,甚至只會被訪問一次。當這些數據服務完訪問請求后,如果還繼續留存在緩存中的話,就只會白白占用內存空間。這種情況,就是緩存污染。

    為了應對緩存污染問題,Redis 從 4.0 版本開始增加了 LFU 淘汰策略。

    LFU 緩存策略是在 LRU 策略基礎上,為每個數據增加了一個「計數器」,來統計這個數據的訪問次數。

    LFU 篩選邏輯
    • 當使用 LFU 策略篩選淘汰數據時,首先會根據數據的訪問次數進行篩選,把訪問次數最低的數據淘汰出緩存。
    • 如果兩個數據的訪問次數相同,LFU 策略再比較這兩個數據的訪問時效性,把距離上一次訪問時間更久的數據淘汰出緩存。
    LFU 的具體實現

    我們在前面說過,為了避免操作鏈表的開銷,Redis 在實現 LRU 策略時使用了兩個近似方法:

    • Redis 在 RedisObject 結構中設置了 lru 字段,用來記錄數據的訪問時間戳。
    • Redis 并沒有為所有的數據維護一個全局的鏈表,而是通過「隨機采樣」方式,選取一定數量的數據放入備選集合,后續在備選集合中根據 lru 字段值的大小進行篩選刪除。

    在此基礎上,Redis 在實現 LFU 策略的時候,只是把原來 24bit 大小的 lru 字段,又進一步拆分成了兩部分:

    • ldt 值:lru 字段的前 16bit,表示數據的訪問時間戳。
    • counter 值:lru 字段的后 8bit,表示數據的訪問次數。

    但是我們會發現一個問題,counter 值的最大記錄值只有255。當幾個緩存數據的 counter 值 都達到255值,就無法正確根據訪問次數來決定數據的淘汰了。

    所以Redis 針對這個問題進行了優化:在實現 LFU 策略時,Redis 并沒有采用數據每被訪問一次,就給對應的 counter 值加 1 的計數規則,而是采用了一個更優化的計數規則。

    Redis 對 LFU 的實現

    Redis 實現 LFU 策略時采用計數規則:

  • 每當數據被訪問一次時,先用「計數器當前的值」乘以「配置項 」lfu_log_factor ,再加 1;取其倒數,得到一個 p 值。
  • 然后,把這個 p 值和一個取值范圍在(0,1)間的隨機數 r 值比大小,只有 p 值大于 r 值時,計數器才加 1。
  • Redis的部分源碼實現如下:

    double r = (double)rand() / RAND_MAX; // 隨機數 r 值 // ...... // baseval 是計數器當前的值,初始值默認是 5,是由代碼中的 LFU_INIT_VAL 常量設置 double p = 1.0 / (baseval * server.lfu_log_factor + 1); // ((計數器當前值 * 配置項參數) + 1 )的倒數 if (r < p) counter++;

    為什么 baseval 的初始值是5,而不是0?是因為這樣可以避免數據剛被寫入緩存,就因為訪問次數少而被立即淘汰。

    使用了這種計算規則后,我們可以通過設置不同的 lfu_log_factor 配置項,來控制計數器值增加的速度,避免 counter 值很快就到 255 了。

    這張表是根據Redis官網獲得的,進一步說明 LFU 策略計數器遞增的效果。
    它記錄了當 lfu_log_factor 取不同值時,在不同的實際訪問次數情況下,計數器值的變化情況。

    lfu_log_factor100 hits1000 hits100K hits1M hits10M hits
    0104255255255255
    11849255255255
    101018142255255
    10081149143255

    通過上表的分析:

    • 當 lfu_log_factor 取值為 1 時,實際訪問次數為 100K 后,counter 值就達到 255 了,無法再區分實際訪問次數更多的數據了。
    • 當 lfu_log_factor 取值為 100 時,當實際訪問次數為 10M 時,counter 值才達到 255。

    使用這種非線性遞增的計數器方法,即使緩存數據的訪問次數成千上萬,LFU 策略也可以有效的區分不同的訪問次數,從而合理的進行數據篩選。

    從剛才的表中,我們可以看到,當 lfu_log_factor 取值為 10 時,百、千、十萬級別的訪問次數對應的 counter 值 已經有明顯的區分了。所以,我們在應用 LFU 策略時,一般可以將 lfu_log_factor 取值為 10。

    但是對于一些業務場景,上方的設計會存在問題:比如說有些數據在「短時間內被大量訪問后就不會再被訪問了」。

    那么再按照訪問次數來篩選的話,這些數據會被留存在緩存中,但不會提升緩存命中率。

    為此,Redis 在實現 LFU 策略時,還設計了一個「 counter 值的衰減機制」。

    LFU 中的 counter 值的衰減機制

    簡單來說,LFU 策略使用 lfu_decay_time(衰減因子配置項) 來控制訪問次數的衰減。

  • LFU 策略會計算當前時間和數據最近一次訪問時間的差值,并把這個差值換算成以分鐘為單位。
  • 然后,LFU 策略再把這個差值除以 lfu_decay_time 值,所得的結果就是數據 counter 要衰減的值。
  • 通過上方的第二點,我們就能知道一個規律,lfu_decay_time 值越大,那么相應的衰減值會變小,衰減效果也會減弱;反之相應的衰減值會變大,衰減效果也會增強。

    所以,如果業務應用中有短時高頻訪問的數據的話,建議把 lfu_decay_time 值設置為 1。

    使用總結

  • 如果業務數據中「有明顯的冷熱數據區分」,建議使用 allkeys-lru 策略。這樣,可以充分利用 LRU 算法的優勢,把最近最常訪問的數據留在緩存中,提升應用的訪問性能。
  • 如果業務應用中的「數據訪問頻率相差不大」,沒有明顯的冷熱數據區分,建議使用 allkeys-random 策略,隨機選擇淘汰的數據。
  • 如果業務中有「置頂」的需求,比如置頂新聞、置頂視頻,那么,可以使用 volatile-lru 策略,同時不給這些置頂數據設置過期時間。這樣一來,這些需要置頂的數據一直不會被刪除,而其他數據會在過期時根據 LRU 規則進行篩選。
  • 事務

    Redis 事務相對于Mysql 事務來說較為簡單,大家可以將二者進行對比,下文也會整理。

    概念

    Redis 事務的本質是一組命令的集合。

    事務支持一次執行多個命令,一個事務中所有命令都會被序列化。在事務執行過程,會按照順序串行化執行隊列中的命令,其他客戶端提交的命令請求不會插入到事務執行命令序列中

    簡單理解,Redis 中的事務,就是具有一次性、順序性、排他性地在命令序列中執行多個命令。

    它的主要作用就是串聯多個命令防止別的命令插隊。

    事務階段

    我們可以把Redis 事務的執行分為三個階段:

  • 開始事務
  • 命令入隊
  • 執行事務
  • 從輸入Multi命令開始,輸入的命令都會依次進入命令隊列中,但不會執行,直到輸入 Exec 后,Redis會將之前的命令隊列中的命令依次執行。組隊的過程中可以通過 discard。

    事務錯誤處理

    事務的錯誤分為兩種情況:

    • 如果組隊中某個命令報出了錯誤,執行時整個的所有隊列都會被取消
    • 如果執行階段某個命令報出了錯誤,則只有報錯的命令不會被執行,而其他的命令都會執行不會回滾

    這說明在 Redis 中,雖然單條命令是原子性執行的,但是事務不保證原子性,且沒有回滾。事務中任意命令執行失敗,其余的命令仍會被執行。

    Watch 監控

    引入

    Redis 中的 悲觀鎖 和 樂觀鎖,簡單提及以下:

    悲觀鎖(Pessimistic Lock),每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

    樂觀鎖(Optimistic Lock),每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量。Redis就是利用這種check-and-set機制實現事務的。

    watch 命令

    在執行 multi 之前,先執行watch key1 [key2],可以監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那么事務將被打斷

    舉例說明:

    假如我賬戶上有100元,此時我們準備再給賬戶充值50元,準備買149元的傳說皮膚。

    但是此時,以一位糟糕的程序員修改了我們的賬戶,改成了999元。

    我很生氣,因為我充值失敗了,但是我去賬戶上一看,變成999元了,我馬上給自己一巴掌,“在生氣什么呢?”…

    模擬上方情景,這是控制臺1的操作:

    模擬上方情景,這是控制臺2的操作:

    注意:只要執行了EXEC,之前加的監控鎖都會被取消!Redis的事務不保證原子性,一條命令執行失敗了,其他的仍然會執行,且不會回滾。

    unwatch 命令

    取消 WATCH 命令對所有 key 的監視。

    如果在執行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被執行了的話,那么就不需要再執行 UNWATCH 了。

    總結說明

    redis 的事務不推薦在實際中使用,如果要使用事務,推薦使用 Lua 腳本,redis 會保證一個 Lua 腳本里的所有命令的原子性。

    Redis集群

    除去Redis的單例模式,Redis 的集群模式可以分為三種:主從復制、哨兵模式、集群模式。

    主從復制

    Redis 官方文檔【主從復制】:REDIS sentinel-old – Redis中國用戶組(CRUG)

    主從復制架構

    主從復制,將 Redis 實例分為兩中角色,一種是被復制的服務器稱為主服務器(master),而對主服務器進行復制的服務器被稱為從服務器(slave)。

    當主數據庫有數據寫入,會將數據同步復制給從節點,一個主數據庫可以同時擁有多個從數據庫,而從數據庫只能擁有一個主數據庫。值得一提的是,從節點也可以有從節點,呈現級聯結構。

    我們可以看到,在主從復制中,只有一個是主機,其他的都是從機,并且從機下面還可以有任意多個從機。

    主數據庫可以進行讀寫操作,從數據庫只能有讀操作(并不一定,只是推薦這么做)。

    開啟主從復制方式

    命令

    通過slaveof 命令,將 127.0.0.1:6380 的redis實例成為 127.0.0.1:6379 的redis實例的從服務器:

    slaveof 127.0.0.1 6379

    測試如下:


    配置

    通過編寫配置文件,例如先為主配置文件命名為 master.conf 進行編寫配置:

    # 通用配置 # bind 127.0.0.1 # 綁定監聽的網卡IP,注釋掉或配置成0.0.0.0可使任意IP均可訪問 port 6379 # 設置監聽端口 #是否開啟保護模式,默認開啟。 # 設置為no之后最好設置一下密碼 protected-mode no #是否在后臺執行,yes:后臺運行;no:不是后臺運行 daemonize yes # 復制選項,slave復制對應的master。 # replicaof <masterip> <masterport> #如果master設置了requirepass,那么slave要連上master,需要有master的密碼才行。masterauth就是用來 # 配置master的密碼,這樣可以在連上master后進行認證。 # masterauth <master-password>

    在啟動節點時輸入命令

    redis-server master.conf redis-server slave1.conf redis-server slave2.conf

    不過在docker容器中的Redis鏡像配置存在一些問題,大家自己找一下資料吧。

    啟動命令

    參考博客鏈接:redis啟動命令及集群創建

    復制的實現【重點】

    1. 設置主服務器的地址和端口

    例如客戶端操作從服務器執行如下命令:

    127.0.0.1> SLAVEOF 127.0.0.1 6379

    從服務器會將客戶端給定的主服務器IP地址以及端口號保存到當前從服務器狀態的 masterhost 屬性和 masterport 屬性中。

    SLAVEOF 命令是一個異步命令,在完成屬性的設置工作后,從服務器會向客戶端返回"OK",之后開始執行真正的復制工作。

    2. 建立套接字連接

    從服務器根據指定的 IP地址和端口號,創建連向主服務器套接字(socket)連接。

    主服務器在接受(accept) 從服務器的套接字連接之后,為該套接字創建相應的客戶端狀態。

    這個時候可以將從服務器理解為主服務器的客戶端。

    3. 發送 PING 命令

    從服務器主服務器發送一個 PING 命令,以檢査套接字的讀寫狀態是否正常、 主服務器能否正常處理命令請求。

    從服務器在發送 PING 命令后,會遇到三種情況:

  • 主服務器響應超時,表示當前兩者之間網絡連接狀態不佳,從服務器重新創建連向主服務器的套接字。
  • 主服務器返回錯誤,表示主服務器暫時無法處理從服務器的命令請求,從服務器重新創建連向主服務器的套接字。
  • 主服務器返回 "PONG",表示主從之間網絡連接狀態正常,主服務器可以正常處理從服務器的命令請求。
  • 4. 身份驗證

    存在這一步的前提是:從服務器設置了 masterauth 選項,那么就要進行這一步的身份驗證,否則跳過。

    從服務器將 masterauth 選項的值封裝成AUTH password 命令并向主服務器發送來進行身份驗證。

    從服務器在身份驗證階段可能會遇到以下幾種情況:

  • 主服務器沒有設置 requirepass 選項,并且從服務器也沒有設置 masterauth 選項,那么繼續執行復制工作。
  • 如果從服務器的 AUTH 命令發送的密碼和主服務器 requirepass 選項的值相同,那么繼續執行復制工作;反之,主服務器返回 invalid password 錯誤。
  • 主服務器設置 requirepass 選項,但是從服務器沒有設置 masterauth 選項,那么主服務器返回 NOAUTH 錯誤;如果主服務器沒有設置 requirepass 選項,但是從服務器設置 masterauth 選項,那么主服務器返回 no password is set錯誤。
  • 5. 發送端口信息

    從服務器主服務器發送當前服務器的監聽端口號, 主服務器收到后記錄在從服務器所對應的客戶端狀態的 slave_listening_port 屬性中。

    執行命令為 REPLCONF listening-port <port-number> ,port-number 即為端口號。

    目前 slave_listening_port 唯一的作用就是在主服務器執行 INFO replication 命令時打印從服務器端口號。

    6. 同步

    從服務器主服務器發送 PSYNC 命令,執行同步操作,此時兩者互為客戶端。

    PSYNC 命令有兩種執行情況:

  • 如果從服務器以前沒有復制過或者執行過 slaveof no one 命令,那么從服務器在開始一次新的復制時,會給主服務器發送 PSYNC ? -1 命令。主動請求進行完整重同步
  • 相反,如果已經復制過,那么從服務器在開始一次新的復制時,將向主服務器發送 PSYNC <runid > <offset> 命令,runid 是上次主服務器的運行ID,offset是從服務器的復制偏移量。
  • 主服務器返回從服務器也有三種情況:

  • 如果主服務器返回 +FULLRESYNC <runid> <offset> 回復,表示主服務器執行完整重同步操作,runid 為主服務器的ID,從服務器會將其保存,offset 是主服務器的復制偏移量,從服務器會將其當作自己的起始復制偏移量。
  • 如果主服務器返回的是 +CONTINUE回復,表示主服務器執行部分重同步操作,從服務器只要等待主服務器發送缺少的那部分數據過來即可。
  • 如果主服務器返回的是 +ERR 回復,那么表示 Redis 版本低于2.8,識別不了 PSYNC 命令,那么從服務器向主服務器發送 SYNC 命令,并與之執行完整同步操作。
  • 從上方可知,主要包括全量數據同步增量數據同步的情況,這跟Redis是否第一次連接和在連接過程中是否離線有關。

    7. 命令傳播

    當完成了同步之后,就會進入命令傳播階段,這時主服務器只要一直將自己執行的寫命令發送給從服務器,而從服務器只要一直接收并執行主服務器發來的寫命令,就可以保證主從一致了。

    主從復制優缺點

    優點
    • 同一個Master可以同步多個Slaves。
    • master能自動將數據同步到slave,可以進行讀寫分離,分擔master的讀壓力
    • master、slave之間的同步是以非阻塞的方式進行的,同步期間,客戶端仍然可以提交查詢或更新請求
    缺點
    • 不具備自動容錯與恢復功能,master或slave的宕機都可能導致客戶端請求失敗,需要等待機器重啟或手動切換客戶端IP才能恢復
    • master宕機,如果宕機前數據沒有同步完,則切換IP后會存在數據不一致的問題
    • 難以支持在線擴容,Redis的容量受限于單機配置

    總結

    其實redis的主從模式很簡單,在實際的生產環境中很少使用,不建議在實際的生產環境中使用主從模式來提供系統的高可用性,之所以不建議使用都是由它的缺點造成的,在數據量非常大的情況,或者對系統的高可用性要求很高的情況下,主從模式也是不穩定的。雖然這個模式很簡單,但是這個模式是其他模式的基礎,所以理解了這個模式,對其他模式的學習會很有幫助。

    命令傳播階段后的心跳檢測 以及 PSYNC 的實現,具體參照書中,不多解釋了。

    哨兵模式

    Redis官方文檔【高可用】:REDIS sentinel-old – Redis中國用戶組(CRUG)

    參考公眾號文章:全面分析Redis高可用的奧秘 - Sentinel

    哨兵模式架構

    哨兵(Sentinel) 是 Redis 的高可用性解決方案:由一個或多個 Sentinel 實例組成的 Sentinel 系統可以監視任意多個主服務器,以及這些主服務器屬下的所有從服務器。

    Sentinel 可以在被監視的主服務器進入下線狀態時,自動將下線主服務器的某個從服務器升級為新的主服務器,然后由新的主服務器代替已下線的主服務器繼續處理命令請求。

    哨兵進程

    哨兵(Sentinel)其實也是Redis 實例,只不過它在啟動時初始化將 Redis 服務器使用的代碼替換成 Sentinel 專用代碼。

    哨兵進程的作用
  • 監控(Monitoring): 哨兵(sentinel) 會不斷地檢查你的Master和Slave是否運作正常。
  • 提醒(Notification):當被監控的某個Redis節點出現問題時, 哨兵(sentinel) 可以通過 API 向管理員或者其他應用程序發送通知。
  • 自動故障遷移(Automatic failover):當一個Master不能正常工作時,哨兵(sentinel) 會開始一次自動故障遷移操作。
  • 哨兵(Sentinel) 和 一般Redis 的區別?

  • Sentinel 的本質只是一個運行在特殊模式下的 Redis 服務器。
  • 一般Redis 初始化時加載RDB 或者 AOF 文件還原數據庫狀態,而Sentinel 不加載是因為它不使用數據庫。
  • Sentinel 使用的代碼是 Sentinel專用代碼。
  • Sentinel 會初始化一個 sentinel.c/sentinelState 結構,用于保存所有和 Sentinel 功能相關的狀態,比如其中的 masters字典記錄了所有被 Sentinel 監視的主服務器相關信息。
  • 哨兵的工作方式

    創建連接

    這一步是初始化 Sentinel 的最后一步,Sentinel 成為主服務器的客戶端,可以向主服務器發送命令。

    每個sentinel都會創建兩個連向主服務器的異步網絡連接

    • 命令連接:用于向master服務發送命令,并接收命令回復。
    • 訂閱連接:用于訂閱、接收master服務的 __sentinel__:hello 頻道。

    為什么有兩個連接?

    命令連接的原因是:Sentinel 必須向主服務器發送命令,以此來與主服務器通信。

    訂閱連接的原因是:目前Redis版本的發布訂閱功能無法保存被發送的信息,如果接收信息的客戶端離線,那么這個客戶端就會丟失這條信息,為了不丟失 __sentinel__:hello 頻道的任何信息,Sentinel 專門用一個訂閱連接來接收該頻道的信息。

    【簡單理解:不僅需要發信息,也需要收信息】

    獲取主服務器信息

    Sentinel 默認會以10秒一次通過命令連接向被監視的主服務器發送 INFO 命令,主服務器收到后回復自己的run_id、IP、端口、對應的主服務器信息及主服務器下的所有從服務器信息。

    Sentinel 根據返回的主服務器信息更新自身的 *masters 實例結構;至于主服務器返回的從服務器信息用于更新對應的slaves 字典列表。

    更新 slaves 字典時有兩種情況:

  • 如果存在從服務器對應的實例結構,那么Sentinel會對該實例結構進行更新。
  • 如果不存在從服務器對應的實例結構,會為這個從服務器新創建一個實例結構。
  • 獲取從服務器信息

    Sentinel 同樣會和從服務器建立異步的命令連接和訂閱連接,并也會默認10秒一次從服務器發送 INFO 命令,從服務器會回復自己的運行run_id、角色role、從服務器復制偏移量offset、主服務器的ip和port、主從服務器連接狀態、從服務器優先級等信息,sentinel會根據返回信息更新對應的 slave 實例結構。

    向主服務器和從服務器發送信息

    Sentinel 默認會以2秒一次通過命令連接向所有被監控的主服務器從服務器的_sentinel:hello頻道發送信息,信息的內容包含兩種參數:

  • 一種參數是以 s_ 開頭的參數,代表 Sentinel 自身的信息。
  • 另一種參數是以 m_ 開頭的參數,代表主服務器的信息。
  • 如果發送的對象是主服務器,那么這些參數就是主服務器的信息。
  • 如果發送的對象是從服務器,那么這些參數就是從服務器正在復制的主服務器信息。
  • 參數列表展示參考:

    參數意義
    s_ipSentinel 的 IP地址
    s_portSentinel 的端口號
    s_runidSentinel 的運行ID
    s_epochSentinel 當前的配置紀元(configuration epoch)
    m_name主服務器的名字
    m_ip主服務器的IP地址
    m_port主服務器的端口號
    m_epoch主服務器當前的配置紀元
    接收來自主服務器和從服務器的頻道信息

    Sentinel通過訂閱連接向服務器發送命令 SUBSCRIBE __sentinel__:hello,保證對_sentinel_:hello的訂閱一直持續到 Sentinel 與 服務器的連接斷開為止。

    _sentinel_:hello頻道 與 Sentinel 的關系是一對多的關系,作用在于發現多個監控同一master的sentinel

    在接收到其他 sentinel 發送的頻道信息后,會根據信息更新 master 對應的 Sentinel 。

    與 master 數據結構綁定后,會建立 Sentinel 與 Sentinel 的命令連接,為后續通訊做準備。

    故障檢測

    檢測主觀下線

    Sentinel 默認會以1秒一次的頻率向與它建立命令連接的所有實例(包括master、slave以及發現的其他sentinel)發送 PING 命令,對方接收后返回兩種回復:

    • **有效回復:**包括運行正常(+PONG)、正在加載(-LOADING)、和主機下線(-MASTERDOWN)。
    • **無效回復:**除有效回復的三種以外都是無效回復,或者在指定時限內沒有返回任何回復。

    在固定時間內,即 down-after-milliseconds(默認單位為毫秒) 配置的時間內收到的都是無效回復,Sentinel 就會標記 master 為主觀下線。與此同時,Sentinel 會將 master 數據結構中對應的flags屬性更新為 SRI_S_DOWN 標識,表示被監控的master在當前sentinel中已經進入主觀下線狀態。

    down-after-milliseconds 的值,不僅是sentinel 用來判斷主服務器主觀下線狀態,還用來判斷主服務器下所有從服務器,以及所有同樣監視這個主服務器的其他Sentinel的主觀下線狀態。

    簡單說明,即 down-after-millsseconds 配置是作用于當前sentinel所監控的所有服務上的,也就是對應master下的slave,以及其他sentinel。另外每個sentinel可以配置不同down-after-millsenconds,所以判定主觀下線的時間也就是不同的。

    檢測客觀下線

    判定 master 為主觀下線狀態的 Sentinel,通過命令詢問其他同樣監控這一主服務器的 Sentinel,看它們是否認為該 master 真的進入了下線狀態。

    Sentinel 發送給其他 Sentinel 的命令為:

    SEBTUBEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

    參數說明:

    • Ip:被 Sentinel 判斷為主觀下線的主服務器的IP地址。
    • port:被 Sentinel 判斷為主觀下線的主服務器的端口號。
    • current_epoch:Sentinel 當前的配置紀元,用于選舉領頭 Sentinel。
    • runid:可以是 * 符號 或 Sentinel 的 run_id,用 * 符號僅用于檢測主服務器的客觀下線狀態;用Sentinel 的 run_id 是用于選舉領頭 Sentinel。

    其他 Sentinel 接收到 SEBTUBEL is-master-down-by-addr 命令后,會根據其中的主服務器IP和端口號,檢查主服務器是否已下線,然后向源 Sentinel 返回一條包含三個參數的 Multi Bulk 回復:

    <down_state> <leader_runid> <leader_epoch>

    參數說明:

    down_state:返回目標 Sentinel 對主服務器的檢查結果,1 代表已下線,0 代表為下線。

    leader_runid:可以是 * 符號 或 目標 Sentinel 的 run_id,用 * 符號僅用于檢測主服務器的下線狀態;用局部領頭 Sentinel 的 run_id 是用于選舉局部領頭 Sentinel。

    leader_epoch:目標 Sentinel 的局部領頭 Sentinel 的配置紀元,用于選舉領頭 Sentinel。【僅在 leader_runid 的值不為 * 時有效,如果 leader_runid 的值為 *,則 leader_epoch 總為0】

    當 Sentinel 收到從其他 Sentinel 返回的足夠數量的已下線判斷之后,Sentinel會將主服務器實例結構的 flags 屬性的 SRI_O_DOWN 標識打開,表示主服務器已經進入客觀下線狀態

    足夠數量的已下線判斷是多少呢?

    不同的 Sentinel 判斷客觀下線狀態的條件是不同的,具體不解釋了,看《Redis設計與實現》P238。

    選舉領頭 Sentinel

    當一個主服務器被判斷為客觀下線時,監測這個下線主服務器的各個 Sentinel 會進行協商,選舉出一個領頭 Sentinel,并由領頭 Sentinel 對下線主服務器執行故障轉移操作。

    下面盡量直白地介紹選舉領頭 Sentinel 的規則和方法:

    • 每個在線的 Sentinel 都有被選為領頭 Sentinel 的資格。

    • 同一個配置紀元內(本質是計數器,在每次選舉后自增一次),每個 Sentinel 都有一次將某個 Sentinel 設置為局部領頭 Sentinel 的機會,并且設置后,在這個配置紀元里不能再更改。

    • 每個發現主服務器進入客觀下線 的Sentinel 都會要求其他 Sentinel 將自己設置為局部領頭Sentinel。

    • 拉票方式為發送 SEBTUBEL is-master-down-by-addr 命令,剛才的 *號替換為源 Sentinel的 run_id,表示希望目標 Sentinel 設置自己為它的局部領頭 Sentinel。

    • 接收拉票命令的目標 Sentinel 可是非常單純,誰的命令先發給它,它就選誰當自己的局部領頭 Sentinel,之后的拉票全部拒絕。

    • 當然,既然目標 Sentinel根據先到先得確定了局部領頭 Sentinel,那也得和大家回個話,它會為發送拉票命令的源 Sentinel 回復命令,記錄了自身選擇的局部領頭 Sentinel的 run_id 和 配置紀元。

    • 如果某個 Sentinel 被半數以上的 Sentinel 設置為了局部領頭 Sentinel,那么這個局部領頭sentinel就變成了領頭sentinel,同一個配置紀元內可能會出現多個局部領頭sentinel,但是領頭sentinel只會產生一個。

    • 如果在給定的時限內,沒有任何一個 Sentinel 被選舉為領頭 Sentinel,那么各個 Sentinel 會在一段時間后再次選舉,直到選出領頭 Sentinel 為止。

    故障遷移

    在選舉出領頭 Sentinel 之后,領頭 Sentinel 會對已下線的主服務器執行故障轉移操作,可分為三個步驟:

  • 在已下線的主服務器下的所有從服務器中,挑選一個從服務器作為新的主服務器。
  • 讓已下線的主服務器下的所有從服務器改為復制新的主服務器。
  • 將已下線的主服務器設置為新的主服務器的從服務器,當它重新上線時會成為新的主服務器的從服務器。
  • 選出新的主服務器

    (一)、新的主服務器是從原主服務器下的從服務器中選擇的,所以需要選擇狀態良好、數據完整的從服務器。領頭 Sentinel 的數據結構中保存了原master對應的 slave ,Sentinel 會刪除狀態較差的slave。過濾執行順序如下:

  • 刪除斷線或者下線的從服務器。
  • 刪除最近 5 秒內沒有回復過領頭 Sentinel 的 INFO 命令的從服務器。
  • 刪除與原 master 斷開超過down-after-millisecond * 10 毫秒的從服務器,這樣可以排除從服務器與原主服務器過早斷開連接,保證備選從服務器的數據都是比較新的。
  • 對應第三條,我可以解釋一下,前面提到過,在 down-after-millisecond 設置的時長內沒有收到有效回復,可以判定當前復制的主服務器主觀下線。所以,越遲和主服務器斷開連接的從服務器,數據越新

    (二)、現在過濾出的都是健康的從服務器了,然后 Sentinel 開始選擇新的主服務器,有以下三個優先級順序:

  • 然后根據從服務器的優先級進行排序,選出優先級最高的服務器。
  • 如果有多個相同最高優先級的從服務器,那么則根據它們的復制偏移量來進行排序。
  • 如果有多個優先級和復制偏移量相同的從服務器,那么選擇 run_id 最小的從服務器。
  • (三)、選出新的主服務器后,領頭 Sentinel 向被選中的從服務器發送 SLAVEOF no one 命令。

    在發送 SLAVEOF no one 命令后,領頭 Sentinel 會以每秒一次的頻率(平時是十秒一次)向被選中的從服務器發送 INFO 命令,當被升級的服務器的 role 字段從 slave 變為 master 時,領頭 Sentinel 就知道它已經順利成為新主服務器了。

    修改從服務器的復制目標

    領頭 Sentinel 給已下線主服務器下的所有從服務器發送 SLAVEOF 命令,讓它們去復制新的主服務器。

    將舊主服務器變為從服務器

    因為舊主服務器下線,領頭Sentinel 會修改它對應主服務器下的實例結構中的設置。

    等舊主服務器重新上線時,Sentinel 就會向它發送 SLAVEOF 命令,讓他成為新的主服務器的從服務器。

    集群模式

    《Redis設計與實現》第十七章 集群 p245;

    官方文檔【集群教程】:REDIS cluster-tutorial – Redis中文資料站 – Redis中國用戶組(CRUG)

    官方文檔【集群規范】:REDIS cluster-spec – Redis中文資料站 – Redis中國用戶組(CRUG)

    官方文檔【分區】:REDIS 分區 – Redis中國用戶組(CRUG)

    集群模式架構

    哨兵模式最大的缺點就是所有的數據都放在一臺服務器上,無法較好的進行水平擴展。

    為了解決哨兵模式的痛點,集群模式應運而生。在高可用上,集群基本是直接復用的哨兵模式的邏輯,并且針對水平擴展進行了優化。

    它具有的特點有:

  • 一個 Redis 集群通常由多個節點(Node)組成。
  • 采取去中心化的集群模式,將數據按槽存儲分布在多個 Redis 節點上。集群共有 16384 個槽,每個節點負責處理部分槽。
  • 使用 CRC16 算法來計算 key 所屬的槽:crc16(key,keylen) & 16383。
  • 所有的 Redis 節點彼此互聯,通過 PING-PONG 機制來進行節點間的心跳檢測。
  • 分片內采用一主多從保證高可用,并提供復制和故障恢復功能。在實際應用場景下,通常會將主從分布在不同服務器,避免單個服務器出現故障導致整個分片出問題,下圖的 內網IP 代表不同的服務器。
  • 客戶端與 Redis 節點直連,不需要中間代理層(proxy)。客戶端不需要連接集群所有節點,連接集群中任何一個可用節點即可。
  • 下面將會根據它的特點逐步說明該集群的核心技術。

    集群數據結構

    使用 clusterNode 結構保存一個節點的當前狀態,比如創建時間、名稱、配置紀元、IP、端口號等。

    每個節點都會為自己和集群中所有其他節點都創建一個對應的 clusterNode 結構來記錄各自的節點狀態。

    struct clusterNode {// 創建節點的時間mstime_t ctime;// 節點的名稱,由40個十六進制字符組成,例如68eef66df23420a5862208ef5...f2ffchar name[REDIS_CLUSTER_NAMELEN];// 節點標識,使用各種不同表示值記錄節點的角色(主節點或從節點);以及節點目前的狀態(在線或下線)int flags;// 節點當前的配置紀元,用于實現故障轉移uint64_t configEpoch;// 節點的IP地址char ip[REDIS_IP_STR_LEN];// 節點的端口號int port;// 保存連接節點所需的相關信息clusterLink *link;// ... };

    其中的 link 屬性是一個 clusterLink 結構,該結構保存連接節點所需的相關信息,包括套接字描述符、輸入緩沖區、輸出緩沖區。

    typedef struct clusterLink {// 連接的創建時間mestime_t ctime;// TCP 套接字描述符int fd;// 輸出緩沖區,保存著待發送給其他節點的信息(message)sds sndbuf;// 輸入緩沖區,保存著從其他節點接收到的信息sds rcvbuf;// 與這個連接相關聯的節點,如果沒有的話就為 NULLstruct clusterNode *node; }

    最后一點,每個節點都保存著一個 clusterState 結構,這個結構記錄了當前節點視角下,所在集群目前所處的狀態。

    例如集群在線或下線狀態、包含節點個數、集群當前的配置紀元等信息。

    typedef struct clsterState {// 指向當前節點的指針clusterNode *myself;// 集群當前的配置紀元,用于實現故障轉移uint64_t currentEpoch;// 集群當前的狀態,是在線還是下線int state;// 集群節點名單(包含myself節點)// 字典的key是節點的名字,value是節點對應的 clusterNode 結構dict *nodes; }

    集群連接方式

    通過發送 CLUSTER MEET 命令,可以讓目標節點A將另一個命令攜帶的節點B添加到目標節點A當前所在的集群中。

    CLUSTER MEET <ip> <port>

    收到命令后開始進行節點A節點B握手階段,以此來確認彼此的存在,為后面的通信打好基礎,該過程簡單說明:

  • 客戶端向節點A發送 CLUSTER MEET 命令后,節點A向節點B發送 MEET 信息,給節點B創建 clusterNode 結構,并更新自己的 clusterState 結構。
  • 節點B返回節點A PONG 信息。
  • 節點A返回節點B PING 信息。
  • 之后,節點A和節點B會通過Gossip 協議傳播給集群其他的節點,讓他們也和節點B握手,最終整個集群達成共識。

    一般集群元數據的維護有兩種方式:集中式、Gossip 協議。在Redis集群中采用Gossip 協議進行通信,所以說它是去中心化的集群。

    下面說一下這兩種方式的區別:

    集中式:是將集群元數據(節點信息、故障等等)幾種存儲在某個節點上。集中式元數據集中存儲的一個典型代表,就是大數據領域的 storm。它是分布式的大數據實時計算引擎,是集中式的元數據存儲的結構,底層基于 zookeeper(分布式協調的中間件)對所有元數據進行存儲維護。

    gossip 協議所有節點都持有一份元數據,不同的節點如果出現了元數據的變更,就不斷將元數據發送給其它的節點,讓其它節點也進行元數據的變更。

    集中式好處在于,元數據的讀取和更新,時效性非常好,一旦元數據出現了變更,就立即更新到集中式的存儲中,其它節點讀取的時候就可以感知到;不好在于,所有的元數據的更新壓力全部集中在一個地方,可能會導致元數據的存儲有壓力。

    gossip 協議好處在于,元數據的更新比較分散,不是集中在一個地方,更新請求會陸陸續續打到所有節點上去更新,降低了壓力;不好在于,元數據的更新有延時,可能導致集群中的一些操作會有一些滯后。

    分布式尋址算法【引入】

    如果會的同學可以跳過,這里只做引申說明。

    一般分布式尋址算法有下列幾種:

    • hash 算法(大量緩存重建)
    • 一致性 hash 算法(自動緩存遷移)+ 虛擬節點(自動負載均衡)
    • redis cluster 的 hash slot 算法
    hash 算法

    來了一個 key,首先計算 hash 值,然后對節點數取模。然后打在不同的 master 節點上。一旦某一個 master 節點宕機,所有請求過來,都會基于最新的剩余 master 節點數去取模,嘗試去取數據。這會導致大部分的請求過來,全部無法拿到有效的緩存,導致大量的流量涌入數據庫。


    一致性 hash 算法

    一致性 hash 算法將整個 hash 值空間組織成一個虛擬的圓環,整個空間按順時針方向組織,下一步將各個 master 節點(使用服務器的 ip 或主機名)進行 hash。這樣就能確定每個節點在其哈希環上的位置

    一致性 hash 算法也是使用取模的方法 hash算法的取模法是對服務器的數量進行取模,而一致性 hash 算法是對 **2^32 ** 取模:

    hash(服務器A的IP地址) % 2^32 hash(服務器B的IP地址) % 2^32 hash(服務器C的IP地址) % 2^32

    來了一個 key,首先計算 hash 值,并確定此數據在環上的位置,從此位置沿環順時針“行走”,遇到的第一個 master 節點就是 key 所在位置。

    使用 hash 算法時,服務器數量發生改變時,所有服務器的所有緩存在同一時間失效了,而使用一致性哈希算法時,服務器的數量如果發生改變,并不是所有緩存都會失效,而是只有部分緩存會失效,例如如果一個節點掛了,受影響的數據僅僅是此節點到環空間前一個節點(沿著逆時針方向行走遇到的第一個節點)之間的數據,其它不受影響。增加一個節點也同理。

    hash 環數據傾斜 & 虛擬節點

    然而當一致性 hash 算法在節點太少或是節點位置分布不均勻時,容易造成大量請求都集中在某一個節點上,而造成緩存熱點的問題。如果i此時該熱點節點出現故障,那么失效緩存的數量也將達到最大值,在極端情況下,有可能引起系統的崩潰,這種情況被稱之為 數據傾斜。

    為了預防 數據傾斜 的問題,一致性 hash 算法引入了虛擬節點機制,即對每一個節點計算多個 hash,每個計算結果位置都放置一個虛擬節點。這樣就實現了數據的均勻分布,負載均衡。

    具體說明,每一個服務節點計算多個哈希,每個計算結果位置都放置一個此服務節點。具體做法可以在服務器ip或主機名的后面增加編號來實現。可以為每臺服務器計算三個虛擬節點,于是可以分別計算 “Node1#1”、“Node1#2”、“Node1#3”、“Node2#1”、“Node2#2”、“Node2#3”的哈希值,這樣可以讓hash 環中存在多個節點,使節點的分布更均勻,當然可以虛擬出更多的虛擬節點,以便減小hash環偏斜所帶來的影響,虛擬節點越多,hash環上的節點就越多,緩存被均勻分布的概率就越大。

    圖就不畫了…理解理解TAT

    hash slot 算法

    redis 集群采用數據分片的哈希槽來進行數據存儲和數據的讀取。

    redis 集群中有固定的 16384 個槽(slot),對每個 key 計算 CRC16 值,然后對 16384 取模,可以獲取 key 對應的 hash slot。

    redis 集群中每個 master 都會被指派部分的槽(slot),假如說當前集群中有3個節點服務器,可能是這樣分配的 [0,5000]、[5001,10000]、[10001,16383]。

    槽位的實現其實就是一個長度為 16384 的二進制數組,根據指定索引位上的二進制位值來判斷節點是否處理指定索引的槽位

    所以槽位的遷移非常簡單:

  • 增加一個 master,就將其他 master 的槽位移動部分過去。
  • 減少一個 master,就將它的槽位移動到其他 master 上去。
  • 移動槽位的成本是非常低的。客戶端的 api,可以對指定的數據,讓他們走同一個槽位,通過 hash tag 來實現。

    在Redis中通過 CLUSTER ADDSLOTS 命令來指派負責的槽位,后面會詳細說明。

    每個節點都會記錄哪些槽指派給了自己,哪些槽指派給了其他節點。客戶端向節點發送鍵命令,節點要計算這個鍵屬于哪個槽。如果是自己負責這個槽,那么直接執行命令,如果不是,向客戶端返回一個 MOVED 錯誤,指引客戶端轉向正確的節點。

    任何一臺機器宕機,另外兩個節點,不影響的。因為 key 找的是 hash slot,不是機器。

    架構圖參照上方《集群模式架構》中。

    可能有人問,為什么一致性hash算法是65535(2^32)個位置,而hash slot 算法卻是16384(2^14)個位置?【翻譯官方回答】

  • 正常的心跳包攜帶節點的完整配置,可以用冪等方式替換舊節點以更新舊配置。 這意味著它們包含原始形式的節點的插槽配置,它使用 16384 個插槽只占用 2k 空間,但使用 65535 個插槽時將占用高達8k 的空間
  • 同時,由于其他設計權衡,Redis Cluster不太可能擴展到超過1000個主節點
  • 因此,16384個插槽處于正確的范圍內,以確保每個主站有足夠的插槽,最多1000個節點,但足夠小的數字可以輕松地將插槽配置傳播為原始位圖。 請注意,在小型集群中,位圖難以壓縮,因為當N很小時,位圖將設置插槽/ N位,這是設置的大部分位。

    一致性 hash 算法 和 hash slot 算法的區別?
    定位規則區別

    它并不是閉合的,key的定位規則是根據 CRC-16(key) % 16384 的值來判斷屬于哪個槽區,從而判斷該key屬于哪個節點,而一致性 hash 算法是根據 hash(key) 的值來順時針找第一個 hash(ip或主機名) 的節點,從而確定key存儲在哪個節點。

    應對熱點緩存區別

    一致性 hash 算法是創建虛擬節點來實現節點宕機后的數據轉移并保證數據的安全性和集群的可用性的。

    redis 集群是采用master節點有多個slave節點機制來保證數據的完整性的。master節點寫入數據,slave節點同步數據。當master節點掛機后,slave節點會通過選舉機制選舉出一個節點變成master節點,實現高可用。但是這里有一點需要考慮,如果master節點存在熱點緩存,某一個時刻某個key的訪問急劇增高,這時該mater節點可能操勞過度而死,隨后從節點選舉為主節點后,同樣宕機,一次類推,造成緩存雪崩。(簡單說明就是,都是被大量請求一套秒的,誰上來都一樣QAQ…)

    擴容和縮容區別

    一致性 hash 算法在新增和刪除節點后,數據會按照順時針自動來重新分布節點

    redis 集群的新增和刪除節點都需要手動來分配槽區

    集群的槽指派

    Redis集群通過分片來保存數據庫的鍵值對:集群整個數據庫被分為16384個槽(slot),數據庫的每個鍵都屬于這16384個槽其中的一個,集群中的每個節點可以處理0個到16384個槽。

    指派節點槽信息

    當集群使用 CLUSTER MEET 命令,整個集群仍處于下線狀態,此時必須通過它們指派槽,通過發送 CLUSTER ADDSLOTS 命令給節點,將一個或多個槽指派給節點負責:

    CLUSTER ADDSLOTS <slot> [slot...]

    比如說將 0 到 5000 個槽指派給節點7000負責:

    CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000

    然后以此類推給其他節點指派槽。

    槽位是在 clusterNode 結構中的 slots 屬性和 numslot 屬性記錄的,記錄當前節點負責處理哪些槽:

    struct clusterNode {//...// 二進制位數組unsigned char slots[16384/8];// 記錄節點負責處理的槽的數量,即slots數組中值為1的二進制位的數量int numslots; }

    在上面小節《分布式尋址算法》的《hash slot 算法》中說過,槽的本質就是一個二進制位數組,通過對[0,16383]上的對應索引為標記來判斷是否處理該槽位:如果slots數組上在指定索引位的二進制位的值為1,標識節點負責處理該槽,反之同理。

    CLUSTER ADDSLOTS 的命令實現

    CLUSTER ADDSLOTS 命令的實現也比較簡單:

  • 遍歷所有輸入槽,檢查它們是否被指派。
  • 只要有一個被指派,那么就返回錯誤并且終止命令執行。
  • 如果都沒有被指派,那么就再次遍歷一遍,將它們指派給當前節點。
  • 設置 clusterState.slot[i]索引位的指針指向 clusterState.myself。(如果不了解它先看下面再回來)
  • 將數組在指定索引位上的二進制設置為1。
  • 執行完畢后,開始廣播通知給集群中的其他節點,自己目前處理的槽位。

    傳播節點槽信息

    節點會將自己的 slots 數組通過消息發送給集群中的其他節點,告知它們自己目前負責的槽位。

    當其他節點接收到消息,會更新自己的在 clusterState.nodes 字典中對應節點的 clusterNode 結構中的 slots 數組。

    記錄集群所有槽的指派信息

    在 clusterState 結構中的 slots 數組記錄了集群中所有 16384 個槽的指派信息:

    typedef struct clusterState {//...clusterNode *slots[16384];//... }

    slots 數組包含 16384 個項,每個數組項都是一個指向 clusterNode 的指針:對應指針指向 NULL 時,說明還未分配;指向 clusterNode 結構時,說明已經指派給了對應結構所代表的節點。

    使用 clusterState.slots 和使用 clusterNode.slots 保存指派信息相比的好處?

    使用clusterState.slots 比使用 clusterNode.slots 能夠更高效地解決問題。

    • 如果只使用 clusterNode.slots來記錄,每次都需要遍歷所有 clusterNode 結構,復雜度為O(N)。
    • 但如果使用 clusterState.slots 來記錄,只需要訪問 clusterState.slots對應的索引位即可,復雜度為O(1)。

    集群執行命令

    建立集群,并且分配完槽位,此時集群就會進入上線狀態,這時候客戶端就可以向集群中的節點發送數據指令了。

    客戶端在向節點發送與數據庫鍵有關的命令時,接收命令的節點就會計算出命令要處理的數據庫鍵屬于哪個槽,并檢查這個槽是否指派個了自己:

    • 如果鍵所在的槽正好指派給當前節點,那么節點就直接執行這個命令
    • 如果鍵所在的槽沒有指派給當前節點,那么節點就會向客戶端返回 MOVED 錯誤指引客戶端向正確的節點,并再次發送之前想要執行的命令。

    節點會使用以下算法來給指定 key 進行計算:

    def slot_number(key):return CRC16(key) & 16383
    • CRC16(key):計算鍵 key 的 CRC-16 校驗和。
    • & 16383:計算出介于0至16383之間的整數作為鍵 key 的槽號。

    當節點計算出鍵所屬的槽后,節點會檢查自己 clusterState.slots 數組中的指定槽位,判斷是否由自己負責:

    • 如果 clusterState.slot[i] 等于 clusterState.myself,說明是由當前節點負責的。
    • 如果 clusterState.slot[i] 不等于 clusterState.myself,說明不是由當前節點負責的,會根據 clusterState.slot[i] 指向的 clusterNode 結構中所記錄的 IP 和 端口號,返回客戶端 MOVED 錯誤,指引客戶端轉向正在處理該槽的節點。
    MOVED 錯誤

    MOVED 錯誤的格式為:

    MOVED <slot> <ip>:<port>
    • slot:鍵所在的槽。
    • ip:port:負責處理該槽節點的IP地址和端口號。

    MOVED 錯誤一般是不會打印的,而是根據該錯誤自動進行節點轉向,并打印轉向信息。

    如果在單機 redis 的情況下,是會被客戶端打印出來的。

    節點數據庫的實現

    節點只能使用0號數據庫,而單機Redis服務器則沒有限制

    節點除了將鍵值對保存在數據庫中之外,還會用 clusterState 結構中的 slots_to_keys跳躍表來保存槽和鍵之間的關系:

    typedef struct clusterState {//...zskiplist *slots_to_keys;//... }

    slots_to_keys 跳表中每個節點的分值(score)都是一個槽位號;每個節點的成員(member)都是一個數據庫鍵。

    • 當節點往數據庫中添加新的鍵值對時,節點會將鍵的槽位號以及這個鍵關聯到 slot_to_keys 跳表中。
    • 當節點刪除數據庫中的某個鍵值對時,節點就會在 slot_to_keys跳表中解除它們的關聯關系。

    重新分片(比如在線擴容)

    Redis 集群的重新分片操作可以將任意數量已經指派給某個節點的槽改為指派給另一個節點,并且相關聯槽位的鍵值對也會從源節點移動到目標節點。

    重新分片的操作是可以在線進行的,保證了高可用

    我們就以在線擴容節點的情況來說吧:比如現在準備在集群中增加一個節點,如何將原有分片中的若干個槽位指派給新添加的節點?

    Redis 集群的重新分片操作是由 Redis 集群管理軟件 redis-trib 負責執行的:Redis 提供重新分配的所有命令,而 redis-trib 通過向源節點和目標接待你發送命令來進行重新分片操作。

    redis-trib 對集群的單個槽進行重新分片的步驟如下:

  • redis-trib給目標節點發送 CLUSTER SETSLOT <slot> IMPORTING <source_id> 命令,讓目標節點準備好從源節點導入對應槽位的鍵值對
  • redis-trib 對源節點發送 CLUSTER SETSLOT <slot> MIGRATING <target_id>命令,讓源節點準備好將對應槽位的鍵值對遷移到目標節點
  • redis-trib 向源節點發送CLUSTER GETKEYSINSLOT <slot> <count> 命令,獲取最多 count 個對應槽的鍵值對的鍵名稱
  • 根據第三步中所獲得的鍵名,redis-trib 都向源節點發送 MIGRATE <target_ip> <target_port> <key_name> 0 <timeout> 命令,將被選中的鍵原子性地遷移到目標節點
  • 重復第三步和第四步,直到源節點中所有對應槽位的鍵值對都遷移到目標節點為止。
  • redis-trib 向集群中的任意一個節點發送 CLUSTER SETSLOT <slot> NODE <target_id> 命令,將對應槽指派給了目標節點,這個信息會被廣播發給整個集群,最終整個集群都知道了對應槽被指派給了目標節點。
  • 如果涉及多個槽,則給每個槽重復執行上述本步驟。

    ASK 錯誤 - (保證集群在線擴容的安全性)

    在重新分片操作期間,可能會出現一部分鍵值對被遷出,一部分鍵值還未被遷出,即在源節點和目標節點都由對應槽的數據

    當節點向源節點發送一個與數據庫鍵相關的命令,并且該鍵的槽位正好處在重新分片的過程中:

  • 源節點現在自己的庫中找指定鍵。
  • 找到的話,直接執行客戶端發送的命令。
  • 沒找到的話,判斷當前源節點是否正在遷移對應數據庫鍵所在的槽位。
  • 如果沒有在遷移,說明鍵不存在,正常執行命令。
  • 如果在遷移,說明鍵有可能在目標節點,返回 ASK 錯誤。
  • ASK 錯誤同 MOVED 錯誤類似,也是不會打印的,也會根據錯誤提供的 IP 和 端口號自動進行轉向操作。

    同理,單機模式下會打印錯誤。

    那 ASK 錯誤 和 MOVED 錯誤有什么區別呢?

    雖然它們能導致客戶端轉向,但是 MOVED 錯誤代表槽的負責權已經交給另一個節點了;而 ASK 錯誤只是兩個節點在遷移槽的過程中使用的臨時措施。

    CLUSTER SETSLOT IMPORTING 命令的實現

    clusterState 結構的 importing_slots_from 數組記錄了當前節點正在從其他節點導入的槽

    typedef struct clusterState {//...clusterNode *importing_slots_from[16384];//... }

    如果 importing_slots_from[i] 的值不為 NULL,而是指向一個 clusterNode 結構,那么表示當前節點正在從 clusterNode 所代表的節點導入該槽。

    在對集群重新分片的時候,向目標節點發送 CLUSTER SETSLOT IMPORTING 命令:

    CLUSTER SETSLOT <slot> IMPORTING <source_id>

    可以將目標節點 clusterState.importing_slots_from[i] 的值設置為 source_id所代表的節點的 clusterNode 結構。

    CLUSTER SETSLOT MIGRATING 命令的實現

    clusterState 結構的 migrating_slots_to 數組記錄了當前節點正在遷移至其他節點的槽

    typedef struct clusterState {//...clusterNode *migrating_slots_to[16384];//... }

    如果 migrating_slots_to[i] 的值不為 NULL,而是指向一個 clusterNode 結構,那么表示當前節點正在將該槽遷移到 clusterNode 所代表的節點。

    ASKING 命令

    當客戶端接收到 ASK 錯誤并轉向正在導入槽的節點時,客戶端會先向節點發送一個 ASKING 命令,然后才重新發送要執行的命令,這是因為客戶端如果不發送 ASKING 命令,而直接發送想要執行的命令的話,那么客戶端發送的命令會被節點拒絕執行,并返回 MOVED 錯誤。

    復制和故障轉移

    Redis 集群中節點可分為主節點(master)和從節點(slave)。

    主節點用于處理槽;從節點用于復制某個主節點,并在主節點下線時,代替下線主節點繼續處理命令請求。

    設置從節點方式

    向一個節點發送命令:

    CLUSTER REPLICATE <node_id>

    可以讓接收命令的節點成為 node_id 所指定的節點的從節點,并開始對主節點進行復制操作,具體步驟如下:

  • 接收命令的節點首先找到 clusterState.nodes 字典中對應 node_id 所對應節點的 clusterNode 結構,并將自身的 clusterState.myself.slaveof 指針指向這個結構,來記錄正在復制的主節點。
  • 修改自身 clusterState,myself.flags 屬性,關閉原來的 REDIS_NODE_MASTER 標識,打開 REDIS_NODE_SLAVE 標識,表明該節點已經從主節點變成從節點。
  • 最后,節點會調用復制代碼對主節點進行復制,相當于向從節點發送 SLAVEOF 命令。
  • 故障檢測

    集群中每個節點都會定期向其他節點發送 PING 信息,以此檢測對方是否在線,如果接收 PING 信息的節點沒有在規定時間內返回 PONG 信息,那么發送消息的節點會將接收消息的節點標記為疑似下線(PFALL)。

    如果在集群中,半數以上負責槽的主節點都將某個主節點標記為疑似下線,那么這個主節點就會被標記為已下線(FALL)。

    將該主節點標記為已下線的節點會向集群廣播關于該節點的 FALL 消息,所有收到這條 FALL 信息的節點都會立即將該節點標記為已下線

    故障轉移

    當一個從節點發現自己正在復制的主節點進入了下線狀態時,從節點會對下線主節點進行故障轉移,按照以下的執行步驟:

  • 從下線主節點的所有從節點中選出一個從節點,讓被選中的從節點執行 SLAVE no one 命令,成為新的主節點。
  • 新的主節點會撤銷所有已下線主節點的槽指派,并將這些槽全部指派給自己
  • 新的主節點向集群廣播 PONG 信息,這條信息可以啊讓其他主節點直到這個節點已經成為主節點,并且接管了所有已下線的主節點負責處理的槽。
  • 新的主節點開始接收自己負責處理的槽相關的命令請求,故障轉移完成。
  • 選舉新的主節點過程

    新的主節點也是通過選舉產生的,簡單介紹一下它的選舉過程:

  • 每一次開始故障轉移操作時,集群的配置紀元(自增計數器,初始值為0)會自增加一。
  • 在每個配置紀元中,集群中每個負責處理槽的主節點都有一次投票機會,而第一個來發送拉票請求的從節點將獲得它的投票。
  • 當從節點發現自己正在復制的主節點已下線時,會向集群廣播 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 信息,要求所有收到信息,并且有投票權的主節點給它投票。
  • 如果一個負責處理槽的主節點尚未投票,在接收到該拉票的 REQUEST 信息時,會返回 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 信息,表示它支持該從節點。
  • 每個參與選舉的從節點都會接收這條 ACK 信息,并且統計自己獲得的支持數。
  • 當一個從節點收集到 N /2 + 1(具有投票權的節點的一半數量加一)時,這個從節點成為新節點。【在一個配置紀元中,只有一個從節點能達到這個數目,確保了主節點只有一個】
  • 如果在這個配置紀元中沒有任何從節點收集到足夠多的支持票,那么會進入下一個配置紀元,并再次進行選舉,直到選出新的主節點為止。
  • 類似于領頭 Sentinel 的選舉,可以對比來看。它們都是基于 Raft 算法的領頭選舉方法來實現的。

    有的小伙伴可能覺得 領頭Sentinel 的選舉不算 Raft,因為它最后是通過領頭 Sentinel 來控制故障遷移的具體過程,這個就是仁者見仁智者見智了。

    Raft 算法的實現可以參考一下Nacos 源碼中 RaftCore 類的實現,比較通俗易懂。有時間我會發一下 Nacos 源碼中Raft選舉的實現。

    Redis應用

    Redis 分布式鎖

    官方文檔:REDIS distlock – Redis中國用戶組(CRUG)

    我最早覺得比較好的實現分布式鎖思路文章:10分鐘精通Redis分布式鎖中的各種門道

    引入

    為什么需要分布式鎖?

    我們在開發項目時,如果需要在同進程內的不同線程并發訪問某項資源,可以使用各種互斥鎖、讀寫鎖

    如果一臺主機上的多個進程需要并發訪問某項資源,則可以使用進程間同步的原語,例如信號量、管道、共享內存等。

    但如果多臺主機需要同時訪問某項資源,就需要使用一種在全局可見并具有互斥性的鎖了。

    這種鎖就是分布式鎖,可以在分布式場景中對資源加鎖,避免競爭資源引起的邏輯錯誤。

    什么時候用分布式鎖?

    一般我們使用分布式鎖有兩個場景:

    • 效率:使用分布式鎖可以避免不同節點重復相同的工作,這些工作會浪費資源。比如用戶注冊后調用發送郵箱的接口發送通知,可能不同節點會發出多封郵箱
    • 安全:加分布式鎖同樣可以避免破壞正確性的發生,如果兩個節點在同一條數據上面操作,比如多個節點機器對同一個訂單操作不同的流程有可能會導致該筆訂單最后狀態出現錯誤,造成損失。
    分布式鎖需要哪些特性呢?

    大部分特性其實都類似于 Java 中的鎖,包括互斥性、可重入、鎖超時、公平鎖和非公平鎖、一致性。

    • 互斥性:在同一時間點,只有一個客戶端持有鎖。
    • 可重入:同一個節點上的同一個線程如果獲取了鎖之后那么也可以再次獲取這個鎖。
    • 鎖超時:在客戶端離線(硬件故障或網絡異常等問題)時,鎖能夠在一段時間后自動釋放防止死鎖,即超時自動解鎖。
    • 公平鎖和非公平鎖:公平鎖即按照請求加鎖的順序獲得鎖,非公平鎖即相反是無序的。
    • 一致性:比如說用Redis 實現分布式鎖時,發生宕機情況,此時會有主從故障轉移的過程中,需要在此過程仍然保持鎖的原狀態。
    • 續鎖:為了防止死鎖大多數會有鎖超時的設置,但是如果業務的執行時間的不確定性,就需要保證在業務仍在執行過程中時,客戶端仍要持有鎖。

    加鎖

    在Redis中加鎖一般都是使用 SET 命令,使用 SET 命令完成 SETNX 和 EXPIRE 操作,并且這是一個原子操作

    set key value [EX seconds] [PX milliseconds] [NX|XX]

    上面這條指令是 SET 指令的使用方式,參數說明如下:

    • key、value:鍵值對。
    • EX seconds:設置失效時長,單位秒。
    • PX milliseconds:設置失效時長,單位毫秒。
    • NX:key不存在時設置value,成功返回OK,失敗返回(nil),SET key value NX 效果等同于 SETNX key value。
    • XX:key存在時設置value,成功返回OK,失敗返回(nil)。

    其中,NX 參數用于保證在多個線程并發 set 下,只會有1個線程成功,起到了鎖的“唯一”性。

    舉例:

    // 設置msg = helloword,失效時長1000ms,不存在時設置 1.1.1.1:6379> set msg helloworld px 1000 nx

    解鎖

    解鎖一般使用 DEL 命令,但是直接刪除鎖可能存在問題。

    一般解鎖需要兩步操作:

  • 查詢當前“鎖”是否還是我們持有,因為存在過期時間,所以可能等你想解鎖的時候,“鎖”已經到期,然后被其他線程獲取了,所以我們在解鎖前需要先判斷自己是否還持有“鎖”。

  • 如果“鎖”還是我們持有,則執行解鎖操作,也就是刪除該鍵值對,并返回成功;否則,直接返回失敗。

  • 由于當前 Redis 還沒有原子命令直接支持這兩步操作,所以當前通常是使用 Lua 腳本來執行解鎖操作,Redis 會保證腳本里的內容執行是一個原子操作

    以下是 Redis 官方給出的 Lua 腳本:

    if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1]) elsereturn 0 end

    參數說明如下:

    • KEYS[1]:我們要解鎖的 key。
    • ARGV[1]:我們加鎖時的 value,用于判斷當“鎖”是否還是我們持有,如果被其他線程持有了,value 就會發生變化。

    續鎖

    一般為了防止死鎖,比如服務器宕機或斷線的情況下無法手動解鎖,此時就需要給分布式鎖加上過期時間

    但是假如在我們業務執行的過程中,Redis 分布式鎖過期了,業務還沒處理完怎么辦?

    首先,我們在設置過期時間時要結合業務場景去設計,盡量設置一個比較合理的值,就是理論上正常處理的話,在這個過期時間內是一定能處理完畢的。

    然后我們需要應對一些特殊惡劣情況進行設計。

    目前的解決方案一般有兩種:

  • 守護線程“續命”:額外起一個線程,定期檢查線程是否還持有鎖,如果有則延長過期時間。Redisson 里面就實現了這個方案,使用“看門狗”定期檢查(每1/3的鎖時間檢查1次),如果線程還持有鎖,則刷新過期時間。
  • 超時回滾:當我們解鎖時發現鎖已經被其他線程獲取了,說明此時我們執行的操作已經是“不安全”的了,此時需要進行事務回滾,并返回失敗。
  • 同時,需要進行告警,人為介入驗證數據的正確性,然后找出超時原因,是否需要對超時時間進行優化等等。

    守護線程“續命”存在的問題

    Redisson 使用看門狗(守護線程)“續命”的方案在大多數場景下是挺不錯的,也被廣泛應用于生產環境,但是在極端情況下還是會存在問題。

    問題例子如下:

  • 線程A首先獲取鎖成功,將鍵值對寫入 Redis 的 master 節點。
  • 在 Redis 將該鍵值對同步到 Slave 節點之前,Master 發生了故障。
  • Redis 觸發故障轉移,其中一個 Slave 升級為新的 master。
  • 此時新的 Master 并不包含線程A寫入的鍵值對,因此線程B嘗試獲取鎖也可以成功拿到鎖。
  • 此時相當于有兩個線程獲取到了鎖,可能會導致各種預期之外的情況發生,例如最常見的臟數據。
  • 解決方法:上述問題的根本原因主要是由于 Redis 異步復制帶來的數據不一致問題導致的,因此解決的方向就是保證數據的一致。

    當前比較主流的解法和思路有兩種:

  • Redis 作者提出的 RedLock。
  • Zookeeper 實現的分布式鎖。
  • 這里我們來說一下第一種 RedLock 的解決思路。

    RedLock

    紅鎖是Redis作者提出的一致性解決方案。紅鎖的本質是一個概率問題:如果一個主從架構的Redis在高可用切換期間丟失鎖的概率是k%,那么相互獨立的 N 個 Redis 同時丟失鎖的概率是多少?如果用紅鎖來實現分布式鎖,那么丟鎖的概率是(k%)^N。鑒于Redis極高的穩定性,此時的概率已經完全能滿足產品的需求。

    說明紅鎖的實現并非這樣嚴格,一般保證M(1<M=<N)個同時鎖上即可,但通常仍舊可以滿足需求。

    RedLock 算法

    算法很易懂,起 5 個 master 節點,分布在不同的機房盡量保證可用性。為了獲得鎖,client 會進行如下操作:

  • 得到當前的時間,微秒單位。
  • 嘗試順序地在 5 個實例上申請鎖,當然需要使用相同的 key 和 random value,這里一個 client 需要合理設置與 master 節點溝通的 timeout 大小,避免長時間和一個 fail 了的節點浪費時間。
  • 當 client 在大于等于 3 個 master 上成功申請到鎖的時候,且它會計算申請鎖消耗了多少時間,這部分消耗的時間采用獲得鎖的當下時間減去第一步獲得的時間戳得到,如果鎖的持續時長(lock validity time)比流逝的時間多的話,那么鎖就真正獲取到了。
  • 如果鎖申請到了,那么鎖真正的 lock validity time 應該是 origin(lock validity time) - 申請鎖期間流逝的時間。
  • 如果 client 申請鎖失敗了,那么它就會在少部分申請成功鎖的 master 節點上執行釋放鎖的操作,重置狀態。
  • 失敗重試

    如果一個 client 申請鎖失敗了,那么它需要稍等一會在重試避免多個 client 同時申請鎖的情況,最好的情況是一個 client 需要幾乎同時向 5 個 master 發起鎖申請。另外就是如果 client 申請鎖失敗了它需要盡快在它曾經申請到鎖的 master 上執行 unlock 操作,便于其他 client 獲得這把鎖,避免這些鎖過期造成的時間浪費,當然如果這時候網絡分區使得 client 無法聯系上這些 master,那么這種浪費就是不得不付出的代價了。

    RedLock 的問題
    • 占用的資源過多,為了實現紅鎖,需要創建多個互不相關的云Redis實例或者自建Redis,成本較高。
    • 嚴重依賴系統時鐘。如果線程1從3個實例獲取到了鎖,但是這3個實例中的某個實例的系統時間走的稍微快一點,則它持有的鎖會提前過期被釋放,當他釋放后,此時又有3個實例是空閑的,則線程2也可以獲取到鎖,則可能出現兩個線程同時持有鎖了。
    • 如果線程1從3個實例獲取到了鎖,但是萬一其中有1臺重啟了,則此時又有3個實例是空閑的,則線程2也可以獲取到鎖,此時又出現兩個線程同時持有鎖了。

    總結

    以上是生活随笔為你收集整理的备战面试日记(6.1) - (缓存相关.Redis全知识点)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    精品国产综合区久久久久久 | 91亚洲精品国偷拍自产在线观看 | 国产精品一区二区三区在线播放 | 激情综合啪啪 | 久久久免费观看完整版 | 久久99精品久久久久久久久久久久 | 91麻豆福利| 久久国产精品一区二区三区四区 | 99精品国产成人一区二区 | 免费看麻豆 | 婷婷久月 | 婷婷六月丁香激情 | 久久99精品久久久久久三级 | 精品九九九| 国产91小视频 | 婷婷激情五月 | 国产精品18毛片一区二区 | 欧美嫩草影院 | 国产精品毛片久久 | 亚洲综合五月天 | 国语久久 | 国产精品黄色 | 亚州成人av在线 | 亚洲v精品 | 亚洲国产成人精品久久 | 国产一区二区免费看 | 免费观看特级毛片 | 成人在线播放视频 | 九九九热 | 91精品久久久久久久久久入口 | 国产精品 日韩 | 国产精品久久99综合免费观看尤物 | 在线中文字幕一区二区 | 日本黄色一级电影 | 亚洲精品久久久久久久蜜桃 | 国产91影视 | 超碰97免费 | 国产一区免费 | 欧美日韩免费看 | 精品999在线 | 久久久国产精品久久久 | 久草在线这里只有精品 | 久草视频资源 | 亚洲精品网站 | 国产在线观看中文字幕 | 午夜精品成人一区二区三区 | 国产精品美 | 9999在线 | a在线一区| 国产视频日韩视频欧美视频 | 久久人人精品 | 欧美日韩另类在线观看 | 亚洲精品午夜视频 | 中文字幕在线观看视频一区 | 一区二区三区在线视频111 | 国产97碰免费视频 | 一区二区欧美日韩 | 五月天婷婷在线观看视频 | av片子在线观看 | 久久综合九色 | 亚洲欧洲中文日韩久久av乱码 | 国产日韩欧美综合在线 | 免费午夜av | 国产精品免费一区二区三区在线观看 | 一级黄视频 | 伊人日日干 | 国产亚洲视频系列 | 国产精品资源在线观看 | 探花系列在线 | 欧美一级视频一区 | 免费观看一级 | 黄色av电影免费观看 | av最新资源 | 五月色婷 | 精品综合久久 | 99视频在线观看视频 | 伊人狠狠色丁香婷婷综合 | 久久理伦片 | 久草精品视频在线看网站免费 | 精品国产自在精品国产精野外直播 | bayu135国产精品视频 | 日本性生活免费看 | 国产精品 中文在线 | 五月婷婷丁香六月 | 丁香婷婷综合激情 | 成人久久久精品国产乱码一区二区 | 亚洲精品久久久久中文字幕二区 | 国产99久久久国产 | av一级久久| 国产成人一区二区在线观看 | 天堂v中文 | 日本成址在线观看 | 国产视频二区三区 | 国产美女精彩久久 | 成人资源在线 | 久久影院一区 | 国产 一区二区三区 在线 | 超碰在线天天 | 久草在| 少妇bbb搡bbbb搡bbbb′ | 六月色丁香 | 欧美va在线观看 | 国产91精品欧美 | 91精品老司机久久一区啪 | 婷五月激情 | 人人澡人人添人人爽一区二区 | 亚洲三级网站 | 久久精品波多野结衣 | 国产在线观看91 | 91九色porn在线资源 | 在线观看av不卡 | 色综合久久66| 亚洲精品免费观看视频 | 日韩欧美区 | 精品一区二区精品 | 黄色com| 国产精品久久久久久久久久免费 | 91亚洲国产 | 久久久一本精品99久久精品 | 免费观看成人网 | 久久深夜| 欧美最新另类人妖 | 久久噜噜少妇网站 | 亚洲国产日本 | 日韩欧美在线高清 | 在线观看av国产 | 97超碰人| 亚洲精品资源在线观看 | 狠狠色狠狠色合久久伊人 | 亚洲精品乱码久久久久久蜜桃不爽 | 免费的黄色av | 在线高清一区 | 国产日韩精品一区二区 | 精品视频在线视频 | 中午字幕在线观看 | 久久99国产一区二区三区 | 97成人资源| 日本中文字幕网 | 伊人亚洲综合网 | 香蕉精品在线观看 | 日韩免费观看一区二区三区 | 日韩特黄一级欧美毛片特黄 | 天天射天天做 | 欧美精品九九99久久 | 超碰com| 蜜臀一区二区三区精品免费视频 | 免费特级黄色片 | 午夜久久 | 经典三级一区 | 国产裸体无遮挡 | 日本韩国中文字幕 | 亚洲女人天堂成人av在线 | 黄色软件视频大全免费下载 | 涩涩爱夜夜爱 | 日韩视频1区| 精品美女在线观看 | 五月激情丁香 | 日韩精品一区二区免费视频 | 九九精品在线观看 | 少妇按摩av | 99久久精品免费视频 | 五月婷婷丁香在线观看 | 最新av网站在线观看 | 中文在线a在线 | 国产在线理论片 | 天堂av在线免费观看 | 久久久久 | 97色噜噜 | 久久人人添人人爽添人人88v | 国产精品18久久久久久久久 | 欧美aa级 | 精品久久一区 | 日韩视频免费 | 四虎在线观看精品视频 | 一区电影 | 丰满少妇在线 | 少妇bbb搡bbbb搡bbbb′ | 国产午夜精品理论片在线 | 国产最新在线观看 | 久久天天躁夜夜躁狠狠85麻豆 | 综合伊人久久 | 久久亚洲精品国产亚洲老地址 | 国产美女视频免费观看的网站 | 亚洲 成人 欧美 | 国产最新在线 | 国产不卡精品视频 | www日 | 99久久精品网 | 亚洲精品国产视频 | 最新日韩精品 | 色老板在线视频 | 国产经典av | 在线电影播放 | 免费看成年人 | 色偷偷网站视频 | 亚洲国产综合在线 | 日本最新中文字幕 | 青青河边草免费直播 | 久久久这里有精品 | 久久综合狠狠综合 | 欧美一级大片在线观看 | 中文字幕无吗 | 成人一级片视频 | 国产精品99久久久久人中文网介绍 | 99精品久久久久久久久久综合 | 国产精品久久婷婷六月丁香 | av在线免费不卡 | 久久伦理电影 | 最近中文字幕免费av | 国产精品精品久久久久久 | 国产色妞影院wwwxxx | 久久久首页 | 免费黄色在线网站 | 岛国片在线 | 久久精品99国产国产 | 免费网址在线播放 | 蜜桃av人人夜夜澡人人爽 | 黄色一级大片在线免费看产 | 天天狠狠 | 亚洲激色| 日韩在线视频在线观看 | 99久久网站 | 欧美黑人巨大xxxxx | a色视频| 国产麻豆精品一区二区 | 婷婷亚洲综合五月天小说 | 日韩xxxxxxxxx| 91超国产| 91tv国产成人福利 | 亚洲国产美女精品久久久久∴ | 国产日韩精品一区二区三区 | 国产午夜视频在线观看 | 久久,天天综合 | 中文字幕中文字幕在线一区 | 天天舔天天射天天操 | 日精品| 91免费观看网站 | 九9热这里真品2 | www.天天干.com | 91色在线观看视频 | 九草在线视频 | 久久福利国产 | 97超级碰碰 | 国产成人精品福利 | 国产无套精品久久久久久 | 久色婷婷 | 看污网站 | 欧美国产日韩在线视频 | 国产日韩精品一区二区三区 | 久久99精品久久久久婷婷 | 国产我不卡 | 奇米影视8888在线观看大全免费 | 国产美女精品视频 | 欧美专区日韩专区 | 国产福利91精品一区 | 成人超碰在线 | 国产精品嫩草55av | 成年人在线播放视频 | 亚洲japanese制服美女 | 97精品欧美91久久久久久 | 国产精品成人av久久 | 91九色在线视频 | 国色天香在线观看 | 99久久99久久精品 | 国产精品91一区 | 91精品欧美一区二区三区 | 日韩欧美一区二区三区免费观看 | 在线视频日韩一区 | 免费观看的黄色片 | 久久精品首页 | www.com.日本一级 | 最新国产一区二区三区 | 美女在线黄 | www·22com天天操 | 日韩欧美一区二区在线观看 | 亚洲精品综合在线观看 | 午夜精品999 | 在线av资源| 成人三级视频 | 精品在线免费视频 | 色人久久| 午夜91视频 | 国产亚洲精品久久19p | 人人干人人添 | 亚洲久草网 | 最新婷婷色 | 亚洲免费在线观看视频 | 日韩在线视频一区二区三区 | 久久99国产精品二区护士 | 精品电影一区 | 天天激情| 精品一区中文字幕 | 国产专区免费 | 九九免费观看全部免费视频 | 欧美综合色在线图区 | 狠狠操导航 | 99视频这里只有 | 欧美一级小视频 | 91精品久久久久久久久久入口 | 色综合久久99 | 狠狠狠狠狠狠操 | 美女av电影| 久久久网站| 国内精品久久久久久久影视简单 | 精品一区二区久久久久久久网站 | 免费在线成人 | 国产亚洲精品久久19p | 亚洲狠狠丁香婷婷综合久久久 | 亚洲精品黄网站 | 久久成人国产精品一区二区 | 狠狠干狠狠色 | 在线草 | 狠狠躁日日躁狂躁夜夜躁 | 欧美日韩视频在线 | 国产在线精品视频 | 国产精品毛片久久久久久久 | 国产精品欧美一区二区 | 国产在线观看二区 | 日韩大片在线看 | 波多野结衣久久资源 | 久草精品视频在线播放 | 五月天综合网站 | 99精品欧美一区二区蜜桃免费 | 国产精品久久久久一区二区三区共 | 国产亚洲精品久久久久5区 成人h电影在线观看 | 欧美日韩国产综合网 | 人人狠狠综合久久亚洲婷 | 天天干,天天操 | 中文字幕免费观看 | 中文字幕av免费在线观看 | 国产99中文字幕 | 91网址在线| 精品国产免费一区二区三区五区 | 伊人狠狠色丁香婷婷综合 | 91精品毛片 | 久久久免费看片 | 国产字幕在线看 | 在线涩涩| 最近日韩免费视频 | 精品特级毛片 | 91久久久久久久一区二区 | av一级在线观看 | 国产精品区二区三区日本 | 亚洲男人天堂2018 | 国产日韩欧美在线观看视频 | 三级在线视频播放 | 在线电影播放 | 欧美性生交大片免网 | 中文字幕日本特黄aa毛片 | 久久伦理网 | 久草视频在线免费 | 涩涩网站在线观看 | 成人免费 在线播放 | 久久久久美女 | 久久国产午夜精品理论片最新版本 | 成人黄色一级视频 | 国产精品久久久久久久久久免费看 | av丝袜在线 | 黄色的网站免费看 | 国产精品成人免费 | 欧美analxxxx| 天天骚夜夜操 | 蜜桃麻豆www久久囤产精品 | 久久久精品 一区二区三区 国产99视频在线观看 | 午夜12点 | 91av视频网 | 一区二区三区久久 | 成人h动漫精品一区二 | 国产视频精品视频 | 九色精品免费永久在线 | 国产99久久九九精品免费 | 免费观看第二部31集 | 日韩成片 | 波多野结衣视频一区二区三区 | 99在线精品观看 | 国产99久久久国产 | av网址aaa| 国产精品毛片一区二区在线 | 国产精品久久久久亚洲影视 | 久久久久久久久久久久av | 亚洲91中文字幕无线码三区 | 久久精品99| 久久不卡日韩美女 | 国产看片网站 | 久久理论视频 | 久久精品久久精品久久39 | 国产成人精品av久久 | 9免费视频| 亚洲一区二区视频在线 | av中文在线| 免费电影一区二区三区 | 亚洲精品视频在线观看视频 | 国产精品久久久久久久av大片 | 国产亚洲片 | 国产色中涩 | 久久久国产精品电影 | 欧美日韩免费一区二区三区 | 特级西西人体444是什么意思 | 国产精品成久久久久 | 麻豆国产电影 | 免费a网址 | 91在线看黄 | 91视频亚洲 | 色com | 天天草天天干天天 | 天天撸夜夜操 | 欧美日韩一区二区在线观看 | 日韩动态视频 | 91福利小视频 | 蜜臀久久99精品久久久久久网站 | 天天草综合网 | 黄色成人小视频 | 超级碰碰碰视频 | 亚洲天堂精品视频在线观看 | 精品久久久国产 | 国产精品久久麻豆 | 精品国产网址 | 人人草在线视频 | 中文字幕xxxx | 亚洲91精品在线观看 | 超碰在线98| 99国产精品一区二区 | 欧美日韩精品在线一区二区 | 免费av网址在线观看 | 久久国产精品影片 | 免费av片在线 | 精品毛片久久久久久 | 黄色成人毛片 | 国产精品综合在线观看 | 在线 视频 一区二区 | 六月婷婷久香在线视频 | 国产中文字幕网 | 99re久久资源最新地址 | 日韩中文字幕免费视频 | 日韩v在线91成人自拍 | 日韩网站在线观看 | av在线影片 | 麻豆视频在线观看 | 狠狠操影视 | 91黄色小网站 | 麻豆国产精品va在线观看不卡 | 黄色大片网 | 97超碰香蕉| 日日躁你夜夜躁你av蜜 | 天堂av网址 | 国产日产精品一区二区三区四区的观看方式 | 在线视频1卡二卡三卡 | 亚洲v欧美v国产v在线观看 | 国产在线观看你懂的 | 亚洲精品在线免费播放 | 婷婷国产一区二区三区 | 日韩欧美国产成人 | 808电影免费观看三年 | 国产小视频免费观看 | 亚洲在线成人精品 | 久久九九精品 | 激情开心网站 | 亚洲国产精彩中文乱码av | 五月天久久婷婷 | 久久久久国产成人精品亚洲午夜 | 国产99久久久国产精品成人免费 | 色综合天天爱 | 欧美激情xxxx | 日日射av | 国产日本在线 | 国产五月| 国产精品福利无圣光在线一区 | 国产高清在线看 | 成人网色 | 久久视屏网 | 中文有码在线 | 日韩精选在线 | 色综合咪咪久久网 | 久久艹在线 | 欧美激情精品久久久久久免费印度 | 亚洲国产成人久久综合 | 久草在线综合 | 最近免费在线观看 | 精品国模一区二区三区 | 亚洲综合视频在线播放 | 操操碰| 国产一级在线看 | 亚洲成av人影片在线观看 | 97久久久免费福利网址 | 久久久久久影视 | 亚洲午夜久久久综合37日本 | 亚洲 欧美 91 | www久久九| 伊人久久国产精品 | 97国产一区二区 | 天天性天天草 | av资源在线观看 | 国产aa免费视频 | 欧美va天堂va视频va在线 | 亚洲精品成人免费 | 免费三级影片 | 欧美另类一二三四区 | 91一区啪爱嗯打偷拍欧美 | 永久免费在线 | 黄污污网站 | 亚州精品一二三区 | 97av影院| 97视频播放| 日韩电影在线观看中文字幕 | www.久草视频 | 少妇性bbb搡bbb爽爽爽欧美 | 国产精品嫩草69影院 | 日韩欧美网站 | 天天干天天干天天色 | 成人a视频在线观看 | 中文字幕高清免费日韩视频在线 | 中文一区在线 | 天天射射天天 | 日韩中文字幕在线观看 | 成人免费精品 | 日韩视频一区二区三区在线播放免费观看 | 国产在线欧美在线 | 中文字幕免费在线看 | 国产一区二区视频在线 | 成人免费观看完整版电影 | 精品久久久久久亚洲综合网站 | 开心激情综合网 | 日韩视频免费看 | 中文字幕免费观看全部电影 | 在线观看岛国片 | 一区二区激情视频 | 一区二区三区四区久久 | 中文一区在线 | 久久免费国产精品 | 81国产精品久久久久久久久久 | 亚洲日本国产精品 | 成人a v视频 | 国产色区| 日韩av电影免费在线观看 | 欧美精品二 | 色噜噜在线观看 | 九九精品在线观看 | 啪啪午夜免费 | 日韩电影在线观看中文字幕 | av线上免费观看 | 国产第一页在线观看 | 国产aa免费视频 | 美女网站色免费 | 99在线观看 | 色就色,综合激情 | 国产伦理一区二区三区 | 精品国产91亚洲一区二区三区www | 蜜臀久久99精品久久久无需会员 | 97av视频在线| 久久精品国产免费看久久精品 | 日韩二级毛片 | 亚洲午夜电影网 | 欧美黑吊大战白妞欧美 | 五月开心激情网 | 99综合视频| 97视频免费在线看 | 久久高清毛片 | 成人动漫一区二区三区 | 亚洲自拍自偷 | 婷婷久久一区二区三区 | 欧美一区二区三区免费观看 | 五月婷婷综 | 天天干夜夜操视频 | 亚洲热视频 | 国产成人三级一区二区在线观看一 | 黄色视屏在线免费观看 | 麻豆国产视频 | 欧美精品久久久久久久久久丰满 | 亚州精品天堂中文字幕 | 精品国自产在线观看 | av手机版| 天天干天天干天天干天天干天天干天天干 | 久久99久久99精品免观看粉嫩 | 色大片免费看 | 激情婷婷在线观看 | 美女久久99 | 永久免费的啪啪网站免费观看浪潮 | 日韩字幕 | www.国产在线视频 | 在线免费观看视频你懂的 | 国产不卡视频 | 色 免费观看 | 四虎www| 91精品福利在线 | 国产免费叼嘿网站免费 | 亚洲视频免费在线观看 | 日韩三级在线 | 免费看十八岁美女 | 视频一区亚洲 | 久久狠狠亚洲综合 | 国产男女爽爽爽免费视频 | av福利在线导航 | 免费一区在线 | 色99视频 | 婷婷综合久久 | 日韩av看片 | 少妇性色午夜淫片aaaze | 久久精品国产亚洲aⅴ | www91在线观看| 日本中文一级片 | av日韩不卡 | 高清久久久久久 | 亚洲 中文 欧美 日韩vr 在线 | 美女视频黄是免费的 | 伊人激情网 | 色综合中文综合网 | 一级黄色在线视频 | 国产一区在线视频 | 五月天综合激情 | 狠狠躁夜夜a产精品视频 | www好男人 | 成年人免费在线 | 久草色在线观看 | 中文字幕精品一区久久久久 | 中文字幕第一页在线播放 | 中文字幕色婷婷在线视频 | 超碰97免费在线 | 国产精品成人自拍 | 欧美另类v | 91视频免费网站 | 国产日韩欧美网站 | a√资源在线 | 国色天香av | 亚洲一级黄色大片 | 国产精品theporn | 黄色免费视频在线观看 | 久二影院| 亚洲综合色婷婷 | 久久黄色网址 | 精品国产99国产精品 | 中文字幕色综合网 | 日韩美一区二区三区 | 久久av免费电影 | 天天操天天色综合 | 日日操日日插 | 久久av免费电影 | 国产又粗又硬又长又爽的视频 | 波多野结衣精品 | 日韩欧美精品一区二区三区经典 | 天天天综合网 | 午夜视频播放 | 蜜桃视频在线视频 | 又黄又网站| 一二三区在线 | 色综合天天做天天爱 | 91精品播放 | 一区二区视频免费在线观看 | av永久网址 | 亚洲资源在线网 | 久久一区二区三区国产精品 | 天天操操操操操 | 国产精品成人久久久久 | 狠狠综合久久av | 国产成人精品一区一区一区 | 免费看的视频 | 色综合天天天天做夜夜夜夜做 | 日本在线中文 | 国产视频在线免费观看 | 天天操天天操天天操天天操天天操 | 一区二区三区国产精品 | 九九热只有精品 | 午夜狠狠干 | 青草草在线视频 | 欧美 亚洲 另类 激情 另类 | 免费av的网站 | 久草视频一区 | 中文字幕 在线 一 二 | 在线观看成年人 | 欧美尹人 | 免费特级黄色片 | 亚洲国产三级在线观看 | 欧美性生交大片免网 | av成人在线播放 | 97在线精品国自产拍中文 | 日韩在线国产精品 | 国产涩涩在线观看 | 午夜三级在线 | 97视频一区| 精品在线视频播放 | 丁香婷婷色综合亚洲电影 | 国产精品成人一区二区三区吃奶 | 99视频久 | 日韩视频一区二区在线 | 天天射天天操天天干 | 最近免费中文视频 | 中文av字幕在线观看 | 在线天堂中文www视软件 | 亚洲爽爽网 | 麻豆视频免费观看 | 日韩欧美高清不卡 | 久操视频在线播放 | 久久午夜色播影院免费高清 | 天天se天天cao天天干 | 97人人超碰在线 | 97av免费视频| 国产精品美女久久久久久久网站 | 伊人网综合在线观看 | 人人精品久久 | 国产中文字幕视频在线观看 | 黄网av在线| 欧美污污视频 | 四虎免费在线观看视频 | 欧美性生活久久 | 国产精品美女在线观看 | 欧美日韩在线看 | 99成人精品 | 精品一区二区6 | 久草视频在线免费播放 | 99在线高清视频在线播放 | 中文字幕亚洲字幕 | 国产精品久久一区二区三区不卡 | 综合网欧美 | 亚洲精品美女视频 | 日韩区在线观看 | 欧美做受高潮电影o | 成人国产精品一区二区 | 岛国精品一区二区 | 91久久久久久久一区二区 | 日韩欧美在线一区二区 | 国产剧情一区在线 | 国产高清视频免费最新在线 | 国精产品满18岁在线 | 国产精品一区二区久久 | 久久影院中文字幕 | 超碰99人人 | 国产又粗又猛又黄又爽 | 久久人人爽人人片 | 欧美黄网站 | 国产免费观看久久黄 | 亚洲成a人片在线www | 久久久精品一区二区 | 中文国产在线观看 | 福利视频第一页 | 欧美在线观看视频 | 国产精品久久久久久久免费观看 | 97超碰人人网 | 五月婷影院 | 国产精品一区二区三区电影 | 天堂视频中文在线 | 9999激情 | av黄色大片 | 国产精品18久久久久久不卡孕妇 | 伊人影院99 | 天天干天天天 | 制服丝袜一区二区 | 国产资源 | 亚洲伦理一区二区 | ww亚洲ww亚在线观看 | 91成人在线观看高潮 | 亚洲欧洲精品一区 | 欧美一二区在线 | 国产91亚洲 | 国产一级电影免费观看 | a黄色片在线观看 | 免费久久99精品国产婷婷六月 | 成人欧美一区二区三区在线观看 | 亚洲精品国精品久久99热一 | 婷婷综合久久 | 国产成人精品久久久久 | 91av视频在线观看 | 在线观看小视频 | av片中文字幕 | 色中色综合 | 亚洲色视频 | 国产免费亚洲高清 | 精品福利在线视频 | 人人超在线公开视频 | 欧美无极色| 干综合网 | 国产色啪 | 视频高清 | 91av免费在线观看 | 国产中文字幕亚洲 | 久久高视频 | 国内精品小视频 | 国产一级在线观看视频 | 亚洲欧美日韩中文在线 | 精品国产乱码久久久久久三级人 | 视频二区| 免费国产在线观看 | 成年人免费在线播放 | 日本一区二区不卡高清 | 欧美-第1页-屁屁影院 | 国产一级性生活 | 国产一级高清视频 | 在线观看免费视频你懂的 | 国产专区视频在线 | 亚洲精品在线观看中文字幕 | 亚洲精品国产精品国自产观看浪潮 | 91看片看淫黄大片 | 97超碰人人澡 | 欧美日韩中文另类 | 国产色综合天天综合网 | 2000xxx影视| 成人一级黄色片 | 黄色a一级视频 | 日韩中文字幕免费在线播放 | 狠狠色噜噜狠狠 | 丝袜av网站 | 九九视频免费在线观看 | 99精品免费 | 亚洲免费视频观看 | 日韩欧美高清 | 亚洲精品国精品久久99热一 | 国产精品福利av | 久久网站最新地址 | 99精品国产兔费观看久久99 | 久久av影院 | 91福利视频一区 | 天天综合网国产 | 91黄色免费看 | 视频三区在线 | 中文字幕乱码一区二区 | 亚洲精品中文在线资源 | 亚洲精品中文字幕视频 | 久久久久一区二区三区 | 在线观看黄色小视频 | 久久精品久久久久电影 | 在线黄色国产电影 | 中文字幕黄网 | 色吊丝av中文字幕 | 国际精品久久 | 亚洲精品综合在线观看 | 欧美日韩综合在线 | wwwwwww色| 免费美女久久99 | 在线视频中文字幕一区 | 欧美日韩性视频 | 国产又粗又猛又色 | 国产精品久久久久三级 | 国产一区成人在线 | 日韩中文字幕亚洲一区二区va在线 | 免费高清在线视频一区· | 一区二区三区在线免费观看视频 | aaawww| 国产69精品久久久久久久久久 | 国产精品一区二区三区免费看 | 欧美日韩国产精品一区二区三区 | 蜜臀久久99精品久久久酒店新书 | 激情五月网站 | 在线观看不卡视频 | 成人在线观看免费视频 | 亚洲精品午夜aaa久久久 | www.久久婷婷 | 丁香高清视频在线看看 | 亚洲黄色小说网址 | 欧美日韩成人一区 | 日本精品一区二区三区在线观看 | 日韩三级av| 美女禁18| 色综合欧洲 | 成人av电影网址 | 黄色精品一区二区 | 91在线免费观看网站 | 超碰在线观看99 | 久草在线免费资源 | 超碰97国产在线 | 中文十次啦 | 久久天堂亚洲 | 国产真实精品久久二三区 | 免费在线黄 | 日韩精品免费在线播放 | 操操日| 91成人精品国产刺激国语对白 | 玖玖在线精品 | 日韩不卡高清视频 | 婷婷六月久久 | 色综合天天色综合 | 亚洲aaa级 | 久久视频这里有久久精品视频11 | 中文字幕欧美日韩va免费视频 | 黄色网址av| 香蕉视频一级 | 久久免费视频国产 | 久久久高清视频 | 国产一区在线视频播放 | 精品一区二区三区久久久 | 亚洲国产精品成人女人久久 | 国产福利av在线 | 欧美成人91 | 欧美日本不卡 | 国产午夜精品av一区二区 | 500部大龄熟乱视频 欧美日本三级 | 国产一区精品在线观看 | 免费网址在线播放 | 亚洲成av人片一区二区梦乃 | 五月天激情综合 | 成人a级黄色片 | 高清日韩一区二区 | 中文字幕在线成人 | 国产成在线观看免费视频 | 国产在线精| 亚洲经典中文字幕 | 天堂激情网 | 国产一区二区三精品久久久无广告 | 国产精品99页 | 国产原厂视频在线观看 | 亚洲91精品在线观看 | 成人资源网| 2019免费中文字幕 | 国产精品系列在线播放 | 久久尤物电影视频在线观看 | 国产97av | 999毛片| 欧美人体xx | 国产精品大片免费观看 | 久久久亚洲成人 | 99久久久久久国产精品 | 国产免费影院 | 亚洲精品久久久蜜桃直播 | 天堂激情网 | 久久免费视频5 | 免费在线91 | 国产精品手机在线 | 深爱激情综合 | 欧美日bb | 一区二区电影在线观看 | 午夜av一区二区三区 | 免费一级毛毛片 | 99精品国产免费久久久久久下载 | 国产精品一区二区av日韩在线 | 人人看97 | 96av在线视频 | 99免费观看视频 | 日日婷婷夜日日天干 | 国产一在线精品一区在线观看 | 国产精品美女久久久久久久久久久 | 日韩经典一区二区三区 | 国产69精品久久久久久久久久 | 国产精品久久久久毛片大屁完整版 | 一区二区三区四区不卡 | 亚洲区视频在线 | 亚洲精品一区二区三区新线路 | 日韩一级电影在线观看 | 五月婷婷激情六月 | av在线小说 | 国产亚洲精品久久久久久电影 | 亚洲 欧洲av | 国产福利免费看 | 欧美高清成人 | 麻豆精品视频 | 日韩精品一区二区三区免费观看视频 | 五月婷婷六月丁香 | 麻豆视频免费在线播放 | 欧美日韩国产一区二区在线观看 | 中文字幕一区二区三区乱码在线 | 91 在线视频播放 | 伊人久操 | 日韩二区三区 | 三上悠亚一区二区在线观看 | 色姑娘综合 | 99在线精品视频 | 久久电影国产免费久久电影 | 亚洲精品永久免费视频 | 91精品国产入口 | 成人观看 | 午夜视频黄| 日韩电影在线观看一区二区 | 日韩免费看片 | 日韩精品一区二区三区中文字幕 | 青青河边草观看完整版高清 | 日日躁天天躁 | 菠萝菠萝蜜在线播放 | 97偷拍视频 | 蜜臀久久99精品久久久无需会员 | 免费网址你懂的 | 深爱激情五月网 | 国产成人久久久77777 | 久久精品99久久久久久2456 | 亚洲乱码精品久久久久 | 人人爽人人澡 | 欧美性色综合 | av午夜电影 | 国产拍在线 | 最新真实国产在线视频 | 毛片区 | 成人国产精品免费观看 | 最近中文字幕在线中文高清版 | 美腿丝袜一区二区三区 | 国际精品久久久久 | 精品你懂的 | www.黄色片网站 | 成人网大片 | 国产精品永久在线 | 伊人久久五月天 | 黄污视频网站大全 | 激情久久久 | 久久综合五月天婷婷伊人 | 波多野结衣视频一区二区 | 成人av电影在线观看 | 精品久久久久久久久亚洲 | 天天曰夜夜爽 | 久久免费视频观看 | 天天操天天谢 | 欧美夫妻生活视频 | 少妇bbbb搡bbbb桶 | 久久久久成人精品 | 中文字幕av在线电影 |