3atv精品不卡视频,97人人超碰国产精品最新,中文字幕av一区二区三区人妻少妇,久久久精品波多野结衣,日韩一区二区三区精品

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

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

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

本人本科畢業(yè),21屆畢業(yè)生,一年工作經(jīng)驗(yàn),簡歷專業(yè)技能如下,現(xiàn)根據(jù)簡歷,并根據(jù)所學(xué)知識(shí)復(fù)習(xí)準(zhǔn)備面試。

記錄日期:2022.1.15

大部分知識(shí)點(diǎn)只做大致介紹,具體內(nèi)容根據(jù)推薦博文鏈接進(jìn)行詳細(xì)復(fù)習(xí)。

文章目錄

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

Redis

書籍推薦:《Redis的設(shè)計(jì)與實(shí)現(xiàn)》

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

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

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

數(shù)據(jù)結(jié)構(gòu)與對(duì)象

數(shù)據(jù)類型分類(對(duì)象)

數(shù)據(jù)類型概述

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

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

另外還有高級(jí)的4種數(shù)據(jù)類型:

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

編碼和底層實(shí)現(xiàn)

主要是講述上述五種基本類型的底層編碼實(shí)現(xiàn):

類型編碼對(duì)象
REDIS_STRINGREDIS_ENCODING_INT使用整數(shù)值來實(shí)現(xiàn)的字符串對(duì)象
REDIS_STRINGREDIS_ENCODING_EMBSTR使用embstr編碼的簡單動(dòng)態(tài)字符串實(shí)現(xiàn)的字符串對(duì)象
REDIS_STRINGREDIS_ENCODING_RAW使用簡單動(dòng)態(tài)字符串實(shí)現(xiàn)的字符串對(duì)象
REDIS_LISTREDIS_ENCODING_ZIPLIST使用壓縮列表實(shí)現(xiàn)的列表對(duì)象
REDIS_LISTREDIS_ENCODING_LINKEDLIST使用雙端鏈表實(shí)現(xiàn)的列表對(duì)象
REDIS_HASHREDIS_ENCODING_ZIPLIST使用壓縮列表實(shí)現(xiàn)的哈希對(duì)象
REDIS_HASHREDIS_ENCODING_HT使用字典實(shí)現(xiàn)的哈希對(duì)象
REDIS_SETREDIS_ENCODING_INTSET使用整數(shù)集合實(shí)現(xiàn)的集合對(duì)象
REDIS_SETREDIS_ENCODING_HT使用字典實(shí)現(xiàn)的集合對(duì)象
REDIS_ZSETREDIS_ENCODING_ZIPLIST使用壓縮列表實(shí)現(xiàn)的有序集合對(duì)象
REDIS_ZSETREDIS_ENCODING_SKIPLIST使用跳躍表字典實(shí)現(xiàn)的有序集合對(duì)象

參考《Redis設(shè)計(jì)與實(shí)現(xiàn)》第一部分 數(shù)據(jù)結(jié)構(gòu)與對(duì)象 的 第八章 對(duì)象,p63。

通過上面的整理我們就可以知道他們的具體編碼實(shí)現(xiàn)了,整理如下:

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

在Redis中我們可以通過 OBJECT ENCODING命令來查看一個(gè)數(shù)據(jù)庫鍵的值對(duì)象的編碼:

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

關(guān)于他們具體在什么時(shí)候使用什么編碼格式,我們?cè)谙挛脑敿?xì)說明!

數(shù)據(jù)結(jié)構(gòu)

主要說明七種對(duì)象:簡單動(dòng)態(tài)字符串、鏈表、字典、跳躍表、整數(shù)集合、壓縮列表。

SDS字符串

簡單動(dòng)態(tài)字符串(SDS),用作Redis的默認(rèn)字符串表示。

SDS定義

每個(gè) sds.h/sdshdr 結(jié)構(gòu)標(biāo)識(shí)一個(gè)SDS值:

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

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

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

SDS 與 C字符串的區(qū)別

如果一張表來說明,即:

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

那我們根據(jù)這五點(diǎn)來說明,這五大區(qū)別的產(chǎn)生原因:

獲取字符串長度

原因如下:

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

其中SDS長度的設(shè)置與更新是由SDS的API執(zhí)行時(shí)自動(dòng)完成的。

緩沖區(qū)溢出

因?yàn)镃字符串沒有記錄字符串長度,所以如果使用如下方法:

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

當(dāng)開發(fā)者已經(jīng)為 dest 字符串分配了一定的內(nèi)存,此時(shí)如果 src 字符串中內(nèi)容拼接進(jìn)去后的內(nèi)存大于分配的內(nèi)存,則會(huì)造成緩沖區(qū)溢出。

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

當(dāng) SDS API 需要對(duì) SDS 進(jìn)行修改時(shí),API 會(huì)先檢查 SDS 的空間是否滿足所需的要求,如果不滿足的話,API 會(huì)自動(dòng)將 SDS 的空間擴(kuò)展至執(zhí)行修改所需的大小,然后才執(zhí)行實(shí)際的修改操作,所以使用 SDS 既不需要后動(dòng)修改 SDS 的空間大小,也不會(huì)出現(xiàn)C字符串中的緩沖區(qū)溢出問題。

內(nèi)存重分配次數(shù)

因?yàn)镃字符串的底層實(shí)現(xiàn)總是 N + 1 個(gè)字符串長度的數(shù)組。所以每次執(zhí)行 增長字符串 或是 縮短字符串時(shí),都要先通過重分配擴(kuò)展底層數(shù)組的空間大小 或是 釋放字符串不再使用的空間,來防止緩沖區(qū)溢出 或者 內(nèi)存泄漏。

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

SDS中使用free屬性記錄未使用空間的字節(jié)數(shù)量。

通過未使用的空間,SDS 實(shí)現(xiàn)了 空間預(yù)分配惰性空間釋放 兩種優(yōu)化策略。

空間預(yù)分配的操作是:當(dāng) SDS 的 API 對(duì)一個(gè) SDS 進(jìn)行修改,并且需要對(duì) SDS 進(jìn)行空間擴(kuò)展的時(shí)候,程序不僅會(huì)為 SDS 分配修改所必須要的空間,還會(huì)為 SDS 分配額外的未使用空間。

這里存在兩種修改情況:

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

    當(dāng)然,如果需要真正地釋放 SDS 的未使用空間,會(huì)有 API 去實(shí)現(xiàn),這里不說明。

    二進(jìn)制安全

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

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

    SDS 的 API 都是二進(jìn)制安全的,所有的 SDS API 都會(huì)以處理二進(jìn)制的方式來處理 SDS 存放的 buf數(shù)組 里的數(shù)據(jù)。

    所以SDS 的 buf屬性被稱為字節(jié)數(shù)組,就是因?yàn)樗怯脕肀4嬉幌盗卸M(jìn)制數(shù)據(jù)。

    兼容< string.h >庫的函數(shù)

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

    鏈表

    鏈表定義

    每個(gè)鏈表節(jié)點(diǎn)使用一個(gè) adlist.h/listNode 結(jié)構(gòu)來表示:

    typedef struct listNode {struct listNode *prev; // 前置指針struct listNode *next; // 后置指針void *value; // 節(jié)點(diǎn)的值 }

    說明該鏈表是一個(gè)雙向鏈表。

    當(dāng)我們使用多個(gè) listNode 組成鏈表,就會(huì)直接使用 adlist.h/list 來持有該鏈表進(jìn)行操作:

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

    字典

    即數(shù)組 + 鏈表實(shí)現(xiàn)。

    字典定義

    Redis 字典所使用的哈希表由 dict.h/dictht 結(jié)構(gòu)定義:

    typedef struct dictht {dictEntry **table; // 哈希表數(shù)組unsigned long size; // 哈希表大小unsigned long sizemask; // 哈希表大小掩碼,用于計(jì)算索引值,總是等于 size - 1unsigned long used; // 哈希表已有節(jié)點(diǎn)數(shù)量 }

    哈希表節(jié)點(diǎn)使用 dictEntry 結(jié)構(gòu)表示,每個(gè) dictEntry 結(jié)構(gòu)都保存著一個(gè)kv對(duì):

    typedef struct dictEntry {void *key; // 鍵union { // 值void *val;uint64_t u64;uint64_t s64;}struct dictEntry *next; // 指向下個(gè)哈希表節(jié)點(diǎn),形成鏈表 }

    Redis 中的字典由 dict.h/dict 結(jié)構(gòu)表示:

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

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

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

    Redis 使用 MurmurHash 算法。

    解決哈希沖突

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

    rehash

    hash 對(duì)象在擴(kuò)容時(shí)使用了一種叫 “漸進(jìn)式 rehash” 的方式。

    rehash概述

    擴(kuò)展收縮哈希表的工作都是通過執(zhí)行 rehash 來完成的。

    reash的步驟如下:

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

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

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

  • 這兩個(gè)哈希表的套路是不是有點(diǎn)像jvm運(yùn)行時(shí)數(shù)據(jù)區(qū)的年輕代的幸存者區(qū)?可以引申一下。

    rehash條件

    當(dāng)下面兩個(gè)條件任意一個(gè)被滿足時(shí),程序就會(huì)自動(dòng)開始對(duì)哈希表進(jìn)行擴(kuò)展操作:

  • 當(dāng)前服務(wù)器沒有在執(zhí)行 BGSAVE 命令或 BGREWRITEAOF 指令,并且哈希表的負(fù)載因子大于等于1。
  • 當(dāng)前服務(wù)器正在執(zhí)行 BGSAVE 命令或 BGREWRITEAOF 指令,并且哈希表的負(fù)載因子大于等于5?!?是因?yàn)橐驯4婀?jié)點(diǎn)數(shù)量包括沖突節(jié)點(diǎn)】
  • 為什么這兩個(gè)命令的是否正在執(zhí)行,和服務(wù)器執(zhí)行擴(kuò)展操作的負(fù)載因子并不相同?

    答:是因?yàn)樵趫?zhí)行BGSAVE命令或者BGREWRITEAOF命令的過程中,Redis需要fork子線程,而大多數(shù)os都采用與時(shí)復(fù)制技術(shù)來優(yōu)化子進(jìn)程的使用效率,所以子進(jìn)程存在的期間,服務(wù)器會(huì)提高執(zhí)行擴(kuò)展操作所需的負(fù)載因子,從而盡可能地避免在子進(jìn)程存在期間進(jìn)行哈希擴(kuò)容,可以避免不必要的內(nèi)存寫入操作,節(jié)約內(nèi)存。

    與時(shí)復(fù)制:copy-on-write,即不用復(fù)制寫入直接引用父進(jìn)程的物理過程。

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

    BGREWRITEAOF命令:異步執(zhí)行AOF重寫,優(yōu)化原文件大小(該命令執(zhí)行失敗不會(huì)丟失數(shù)據(jù),成功才會(huì)真正修改數(shù)據(jù),2.4以后手動(dòng)觸發(fā)該命令)

    漸進(jìn)式hash過程

    漸進(jìn)式rehash的詳細(xì)步驟:

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

    漸進(jìn)式hash執(zhí)行期間進(jìn)行哈希表操作
  • 進(jìn)行刪除、查找、更新操作時(shí),都會(huì)在兩個(gè)哈希表上進(jìn)行。比如說查找操作,現(xiàn)在ht[0]上查找,如果ht[0]上沒有就去ht[1]上查找。
  • 進(jìn)行添加操作時(shí),新的鍵值對(duì)直接保存在ht[1]中,而ht[0]不進(jìn)行操作,這樣保證ht[0]只減不增。
  • 漸進(jìn)式hash的缺點(diǎn)
  • 擴(kuò)容期開始時(shí),會(huì)先給 ht[1] 申請(qǐng)空間,所以在整個(gè)擴(kuò)容期間,會(huì)同時(shí)存在 ht[0] 和 ht[1],會(huì)占用額外的空間。

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

  • redis 在內(nèi)存使用接近 maxmemory 并且有設(shè)置驅(qū)逐策略的情況下,出現(xiàn) rehash 會(huì)使得內(nèi)存占用超過 maxmemory,觸發(fā)驅(qū)逐淘汰操作,導(dǎo)致 master/slave 均有有大量的 key 被驅(qū)逐淘汰,從而出現(xiàn) master/slave 主從不一致。

  • 跳躍表

    可以把他理解為一個(gè)可以二分查找的鏈表。

    它在Redis中只用到過兩處:一是有序集合zset;二是集群節(jié)點(diǎn)的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。

    這塊的實(shí)現(xiàn)就不整理,看博客 或者 看書吧,《Redis設(shè)計(jì)與實(shí)現(xiàn)》p38。

    參考博客鏈接一:面試準(zhǔn)備 – Redis 跳躍表

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

    參考博客鏈接三:跳躍表以及跳躍表在redis中的實(shí)現(xiàn)

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

    整數(shù)集合

    整數(shù)集合定義

    每個(gè) intset.h/intset 結(jié)構(gòu)表示一個(gè)整數(shù)集合:

    typedef struct intset {uint32_t encoding; // 編碼方式uint32_t length; // 集合包含的元素?cái)?shù)量int8_t contents[]; // 保存元素的數(shù)組 }

    其中 contents[]就是整數(shù)集合的底層實(shí)現(xiàn):整數(shù)集合的每個(gè)元素都是該數(shù)組的一個(gè)數(shù)組項(xiàng),各個(gè)項(xiàng)在數(shù)組中是從小到大有序排列,并且不重復(fù)。

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

    整數(shù)集合升級(jí)

    整數(shù)升級(jí),即當(dāng)我們將一個(gè)新元素添加到集合中時(shí),新元素的類型比原集合的類型都要長時(shí),整數(shù)集合需要升級(jí),然后才能將新元素添加到集合中。

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

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

    升級(jí)的好處
  • 提升整數(shù)集合的靈活性。
  • 盡可能節(jié)約內(nèi)存。
  • 整數(shù)集合降級(jí)

    整數(shù)集合不支持降級(jí)操作!

    壓縮列表

    它的存在意義就是為了節(jié)約內(nèi)存

    壓縮列表定義

    壓縮列表就是一個(gè)由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型數(shù)據(jù)結(jié)構(gòu)。

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

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

    每個(gè)壓縮列表節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或者一個(gè)整數(shù)值。其中,字節(jié)數(shù)組可以是以下三種長度之一:

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

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

    • 4位長,介于0至12之間的無符號(hào)整數(shù);
    • 1字節(jié)長的有符號(hào);
    • 3字節(jié)長的有符號(hào)整數(shù);
    • int16_t類型整數(shù);
    • int32_t類型整數(shù);
    • int64_t類型整數(shù)。

    每個(gè)壓縮列表節(jié)點(diǎn)都由 previous_entry_length、encoding、content三個(gè)部分組成:

    previous_entry_length

    節(jié)點(diǎn)的 previous_entry_length 屬性以字節(jié)為單位,記錄了壓縮列表中前一個(gè)節(jié)點(diǎn)的長度

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

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

    它的好處就是,因?yàn)楣?jié)點(diǎn)的 previous_entry_length 屬性記錄了前一個(gè)節(jié)點(diǎn)的長度,所以程序可以通過指針運(yùn)算,根據(jù)當(dāng)前節(jié)點(diǎn)的起始地址來計(jì)算出前一節(jié)點(diǎn)的起始地址。

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

    encoding

    節(jié)點(diǎn)的 encoding 屬性記錄了節(jié)點(diǎn)的 content 屬性所保存數(shù)據(jù)的類型以及長度

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

    節(jié)點(diǎn)的 content 屬性負(fù)責(zé)保存節(jié)點(diǎn)的值,節(jié)點(diǎn)值可以是一個(gè)字節(jié)數(shù)組或者整數(shù)值,值的類型和長度由節(jié)點(diǎn)的 encoding 屬性決定。

    連鎖更新

    redis中的壓縮列表在插入數(shù)據(jù)的時(shí)候可能存在連鎖擴(kuò)容的情況。

    在壓縮列表中,節(jié)點(diǎn)需要存放上一個(gè)節(jié)點(diǎn)的長度:當(dāng)上一個(gè)entry節(jié)點(diǎn)長度小于254個(gè)字節(jié)的時(shí)候,將會(huì)一個(gè)字節(jié)的大小來存放entry中的數(shù)據(jù);但是當(dāng)上一個(gè)entry節(jié)點(diǎn)長度大于等于254個(gè)字節(jié)的時(shí)候,就會(huì)需要更大的空間來存放數(shù)據(jù)。

    在壓縮列表中,會(huì)把大于等于254字節(jié)長度用5個(gè)字節(jié)來存儲(chǔ),第一個(gè)字節(jié)是254,當(dāng)讀到254的時(shí)候,將會(huì)確認(rèn)接下來的4個(gè)字節(jié)大小將是entry的長度數(shù)據(jù)。當(dāng)?shù)谝粋€(gè)字節(jié)為255的時(shí)候,就證明壓縮列表已經(jīng)到達(dá)末端。

    由于表示長度的字節(jié)大小不一樣,當(dāng)新節(jié)點(diǎn)的插入可能會(huì)導(dǎo)致下一個(gè)節(jié)點(diǎn)原本存放表示上一節(jié)點(diǎn)的長度的空間大小不夠?qū)е滦枰獢U(kuò)容這一字段。相應(yīng)的該字段將會(huì)由一個(gè)字節(jié)擴(kuò)容到五個(gè)字節(jié),四個(gè)字節(jié)的長度變化,當(dāng)發(fā)生變化的節(jié)點(diǎn)原本長度在250到253之間的時(shí)候,將會(huì)導(dǎo)致下一個(gè)節(jié)點(diǎn)存儲(chǔ)上節(jié)點(diǎn)長度的空間發(fā)生變化,引起一個(gè)連鎖擴(kuò)容的情況,這一情況將會(huì)直到一個(gè)不需要擴(kuò)容的節(jié)點(diǎn)為止。

    擴(kuò)容邏輯代碼如下,可參考:

    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;} }

    代碼邏輯是:首先,從新插入的節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)開始,如果下一個(gè)節(jié)點(diǎn)存放上一個(gè)字節(jié)的空間大小大于或等于當(dāng)前的節(jié)點(diǎn)長度,那么在存放了這一長度數(shù)據(jù)之后,該次連鎖擴(kuò)容直接宣告結(jié)束。如果下一個(gè)節(jié)點(diǎn)存放長度的空間不能容納當(dāng)前節(jié)點(diǎn)的長度,那么就會(huì)將下一個(gè)節(jié)點(diǎn)進(jìn)行擴(kuò)容,并重新申請(qǐng)內(nèi)存大小,并復(fù)制數(shù)據(jù),移動(dòng)指向尾部節(jié)點(diǎn)的指針。最后移動(dòng)到下一個(gè)節(jié)點(diǎn),在下一個(gè)循環(huán)中判斷是否需要繼續(xù)擴(kuò)容。

    編碼轉(zhuǎn)換時(shí)機(jī)

    Redis中的每個(gè)對(duì)象都由一個(gè) redisObject 結(jié)構(gòu)來表示:

    typedef struct redisObject {unsigned type:4; // 類型unsigned encoding:4; // 編碼void *ptr; // 指向底層實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)的指針 }

    類型包括基本的五種,編碼指對(duì)應(yīng)類型下的不同編碼實(shí)現(xiàn)。

    Redis可以根據(jù)不同的使用場景,來為一個(gè)對(duì)象設(shè)置不同的編碼,從而優(yōu)化對(duì)象在某一場景下的效率。

    字符串

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

    int

    如果一個(gè)字符串對(duì)象保存的是整數(shù)值,并且這個(gè)整數(shù)值可以用long類型來表示,那么這個(gè)字符串對(duì)象會(huì)將整數(shù)值保存在字符串對(duì)象結(jié)構(gòu)的 ptr 屬性中(將 void* 轉(zhuǎn)換成 long),并將字符串對(duì)象的編碼設(shè)置為int。

    raw

    如果一個(gè)字符串對(duì)象保存的是一個(gè)字符串值,并且長度大于44字節(jié),那么這個(gè)字符串對(duì)象將使用簡單動(dòng)態(tài)字符串(SDS)來保存,并且編碼設(shè)置為 raw。

    embstr

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

    embstr 是專門用于保存短字符串的優(yōu)化編碼方式。它和 raw 的區(qū)別在于,raw編碼會(huì)調(diào)用兩次內(nèi)存分配函數(shù)來分別創(chuàng)建 redisObject 和 sdshdr 結(jié)構(gòu),而embstr 編碼則通過調(diào)用一次內(nèi)存分配函數(shù)來分配一塊連續(xù)的空間,空間中依次包含 redisObject 和 sdshdr 結(jié)構(gòu)。

    使用 embstr 的好處:

  • 內(nèi)存分配次數(shù)減少一次。
  • 釋放內(nèi)存時(shí)的調(diào)用函數(shù)次數(shù)也少一次。
  • embstr 保存在連續(xù)的內(nèi)存中,它可以更好地利用緩存帶來的優(yōu)勢(shì)。
  • 不過,embstr 編碼沒有任何相應(yīng)的修改程序,它實(shí)際上只是只讀的,當(dāng) embstr 編碼的字符串執(zhí)行修改命令時(shí),總會(huì)變成 raw。

    為什么raw和embstr的臨界值是44字節(jié)?

    如果看過書的同學(xué)有疑問很正常,因?yàn)樵凇禦edis的設(shè)計(jì)與實(shí)現(xiàn)》中,它寫的臨界值是39字節(jié),但是實(shí)際上經(jīng)過查找資料,在3.2版本之后就改成了44字節(jié)了。主要原因是為了內(nèi)存優(yōu)化,具體解釋如下:

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

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

    列表

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

    ziplist

    如果列表對(duì)象保存的所有字符串元素的長度都小于64字節(jié),并且列表對(duì)象保存的元素?cái)?shù)量小于512個(gè)時(shí),編碼為 ziplist。

    linkedlist

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

    哈希

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

    ziplist

    如果哈希對(duì)象保存的所有鍵值對(duì)的鍵和值的字符串長度都小于64字節(jié),并且哈希對(duì)象保存的鍵值對(duì)數(shù)量小于512個(gè)時(shí),編碼為 ziplist。

    hashtable

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

    集合

    集合對(duì)象的編碼可以是 intset 或者 hashtable。

    intset

    如果集合對(duì)象保存的所有元素都是整數(shù)值,并且哈希對(duì)象保存的元素?cái)?shù)量小于512個(gè)時(shí),編碼為 intset。

    hashtable

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

    有序集合

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

    ziplist

    如果有序集合對(duì)象保存的所有元素成員的長度都小于64字節(jié),并且有序集合對(duì)象保存的元素?cái)?shù)量小于128個(gè)時(shí),編碼為 ziplist。

    skiplist

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

    持久化

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

    Redis對(duì)數(shù)據(jù)的操作都是基于內(nèi)存的,當(dāng)遇到了進(jìn)程退出、服務(wù)器宕機(jī)等意外情況,如果沒有持久化機(jī)制,那么Redis中的數(shù)據(jù)將會(huì)丟失無法恢復(fù)。有了持久化機(jī)制,Redis在下次重啟時(shí)可以利用之前持久化的文件進(jìn)行數(shù)據(jù)恢復(fù)。

    Redis支持的兩種持久化機(jī)制:

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

    RDB

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

    觸發(fā)方式

    手動(dòng)觸發(fā)

    save 和 bgsave 命令都可以手動(dòng)觸發(fā)RDB持久化。

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

    簡單說明,save命令會(huì)全程阻塞,bgsave只在創(chuàng)建子線程時(shí)會(huì)阻塞。

    自動(dòng)觸發(fā)

    在以下幾種場景下,會(huì)自動(dòng)觸發(fā)RDB持久化:

  • 在配置文件中設(shè)置了 save 的相關(guān)配置,如sava m n,它表示在 m 秒內(nèi)數(shù)據(jù)被修改過 n 次時(shí),自動(dòng)觸發(fā) bgsave 操作。
  • 當(dāng)從節(jié)點(diǎn)做全量復(fù)制時(shí),主節(jié)點(diǎn)會(huì)自動(dòng)執(zhí)行 bgsave 操作,并且把生成的RDB文件發(fā)送給從節(jié)點(diǎn)。
  • 執(zhí)行 debug reload 命令時(shí),也會(huì)自動(dòng)觸發(fā) bgsave 操作。
  • 執(zhí)行 shutdown 命令時(shí),如果沒有開啟AOF持久化也會(huì)自動(dòng)觸發(fā) bgsave 操作。
  • RDB優(yōu)缺點(diǎn)

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

    執(zhí)行流程

  • 命令追加(append):所有寫命令都會(huì)被追加到AOF緩存區(qū)(aof_buf)中。
  • 文件同步(sync):根據(jù)不同策略將AOF緩存區(qū)同步到AOF文件中。
  • 文件重寫(rewrite):定期對(duì)AOF文件進(jìn)行重寫,以達(dá)到壓縮的目的。
  • 數(shù)據(jù)加載(load):當(dāng)需要恢復(fù)數(shù)據(jù)時(shí),重新執(zhí)行AOF文件中的命令。
  • 觸發(fā)方式

    手動(dòng)觸發(fā)

    使用 bgrewriteaof 命令。

    自動(dòng)觸發(fā)

    根據(jù) auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 配置確定自動(dòng)觸發(fā)的時(shí)機(jī)。

    • auto-aof-rewrite-min-size 表示運(yùn)行AOF重寫時(shí)文件大小的最小值,默認(rèn)為64MB。
    • auto-aof-rewrite-percentage 表示當(dāng)前AOF文件大小和上一次重寫后AOF文件大小的比值的最小值,默認(rèn)為100。

    只用前兩者同時(shí)超過閾值時(shí)才會(huì)自動(dòng)觸發(fā)文件重寫。

    AOF文件同步策略

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

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

    AOF持久化配置

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

    文件事件處理器

    推薦博客文章:Redis全面解析一:redis是單線程結(jié)構(gòu)為何還可以支持高并發(fā)

    我們經(jīng)常說Redis是單線程的,但是為什么這么說呢?

    因?yàn)?Redis 內(nèi)部用的是基于 Reactor 模式開發(fā)的文件事件處理器,文件事件處理器是以單線程方式運(yùn)行的,所以redis才叫單線程模型。

    組成部分

    基于 Reactor 模式設(shè)計(jì)的四個(gè)組成部分的結(jié)構(gòu)如下所示:

    它們分別是:

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

    處理機(jī)制

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

  • 每一個(gè)套接字準(zhǔn)備好執(zhí)行連接應(yīng)答、寫入、讀取、關(guān)閉等操作時(shí),就會(huì)產(chǎn)生一個(gè)文件事件。一個(gè)服務(wù)器會(huì)連接多個(gè)套接字,多個(gè)文件事件并發(fā)的出現(xiàn)。
  • I/O多路復(fù)用程序負(fù)責(zé)監(jiān)聽多個(gè)套接字,并向文件事件分派器傳送那些產(chǎn)生的套接字,I/O多路復(fù)用程序會(huì)將所有產(chǎn)生事件的套接字都放到一個(gè)隊(duì)列里面,然后通過這個(gè)隊(duì)列,以有序、同步、每次一個(gè)套接字的方式向文件事件分派器傳送套接字。當(dāng)上一個(gè)套接字處理完畢,接受下一個(gè)套接字。
  • 文件事件分派器接收I/O多路復(fù)用程序傳來的套接字,并根據(jù)套接字產(chǎn)生的事件的類型,調(diào)用相應(yīng)的事件處理器。(執(zhí)行不同任務(wù)的套接字關(guān)聯(lián)不同的事件處理器)。
  • 拓展

    關(guān)于Redis6.0的多線程升級(jí)博客參考鏈接:Redis6 新特性多線程解析

    內(nèi)存淘汰機(jī)制

    Redis 緩存使用內(nèi)存保存數(shù)據(jù),避免了系統(tǒng)直接從后臺(tái)數(shù)據(jù)庫讀取數(shù)據(jù),提高了響應(yīng)速度。由于緩存容量有限,當(dāng)緩存容量到達(dá)上限,就需要?jiǎng)h除部分?jǐn)?shù)據(jù)挪出空間,這樣新數(shù)據(jù)才可以添加進(jìn)來。Redis 定義了「淘汰機(jī)制」用來解決內(nèi)存被寫滿的問題。

    緩存淘汰機(jī)制,也叫緩存替換機(jī)制,它需要解決兩個(gè)問題:

    • 決定淘汰哪些數(shù)據(jù)。
    • 如何處理那些被淘汰的數(shù)據(jù)。

    內(nèi)存淘汰策略

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

    • noeviction:不會(huì)淘汰任何數(shù)據(jù),當(dāng)使用的內(nèi)存空間超過 maxmemory 值時(shí),返回錯(cuò)誤。
    • volatile-ttl:篩選設(shè)置了過期時(shí)間的鍵值對(duì),越早過期的越先被刪除。
    • volatile-random:篩選設(shè)置了過期時(shí)間的鍵值對(duì),隨機(jī)刪除。
    • volatile-lru:使用 LRU 算法篩選設(shè)置了過期時(shí)間的鍵值對(duì)。
    • volatile-lfu:使用 LFU 算法選擇設(shè)置了過期時(shí)間的鍵值對(duì)。
    • allkeys-random:在所有鍵值對(duì)中,隨機(jī)選擇并刪除數(shù)據(jù)。
    • allkeys-lru:使用 LRU 算法在所有數(shù)據(jù)中進(jìn)行篩選。
    • allkeys-lfu:使用 LFU 算法在所有數(shù)據(jù)中進(jìn)行篩選。

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

    • 不淘汰數(shù)據(jù):noeviction。
    • 淘汰數(shù)據(jù)
      • 設(shè)置了過期時(shí)間的鍵值對(duì)中進(jìn)行淘汰:volatile-ttl、volatile-random、volatile-lru、volatile-lfu。
      • 對(duì)所有數(shù)據(jù)進(jìn)行淘汰:allkeys-random、allkeys-lru、allkeys-lfu。

    策略介紹

    noeviction

    noeviction 策略,也是 Redis 的默認(rèn)策略,它要求 Redis 在使用的內(nèi)存空間超過 maxmemory 值時(shí),也不進(jìn)行數(shù)據(jù)淘汰。一旦緩存被寫滿了,再有寫請(qǐng)求來的時(shí)候,Redis 會(huì)直接返回錯(cuò)誤。

    我們實(shí)際項(xiàng)目中,一般不會(huì)使用這種策略。因?yàn)槲覀儤I(yè)務(wù)數(shù)據(jù)量通常會(huì)超過緩存容量的,而這個(gè)策略不淘汰數(shù)據(jù),導(dǎo)致有些熱點(diǎn)數(shù)據(jù)保存不到緩存中,失去了使用緩存的初衷。

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

    volatile-random、volatile-ttl、volatile-lru、volatile-lfu 這四種淘汰策略。它們淘汰數(shù)據(jù)的時(shí)候,只會(huì)篩選設(shè)置了過期時(shí)間的鍵值對(duì)上。

    比如,我們使用 EXPIRE 命令對(duì)一批鍵值對(duì)設(shè)置了過期時(shí)間,那么會(huì)有兩種情況會(huì)對(duì)這些數(shù)據(jù)進(jìn)行清理:

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

    allkeys-random、allkeys-lru、allkeys-lfu

    allkeys-random,allkeys-lru,allkeys-lfu 這三種策略跟上述四種策略的區(qū)別是:淘汰時(shí)數(shù)據(jù)篩選的數(shù)據(jù)范圍是所有鍵值對(duì)。

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

    LRU & LFU算法

    LRU

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

    LRU 篩選邏輯

    RU 會(huì)把所有的數(shù)據(jù)組織成一個(gè)鏈表,鏈表的頭和尾分別表示 MRU 端和 LRU 端,分別代表「最近最常使用」的數(shù)據(jù)和「最近最不常用」的數(shù)據(jù)。

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

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

  • 將新數(shù)據(jù)放到MRU端。
  • 將LRU端的數(shù)據(jù)刪除。
  • 簡單說明,即它認(rèn)為剛剛被訪問的數(shù)據(jù),肯定還會(huì)被再次訪問,所以就把它放在 MRU端;LRU 端的數(shù)據(jù)被認(rèn)為是長久不訪問的數(shù)據(jù),在緩存滿時(shí),就優(yōu)先刪除它。

    Redis 對(duì) LRU 的實(shí)現(xiàn)

    Redis 3.0 前,隨機(jī)選取 N 個(gè)淘汰法。

    Redis 默認(rèn)會(huì)記錄每個(gè)數(shù)據(jù)的最近一次訪問的時(shí)間戳(由鍵值對(duì)數(shù)據(jù)結(jié)構(gòu) RedisObject 中的 lru 字段記錄)。

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

    config set maxmemory-samples 100

    當(dāng)需要再次淘汰數(shù)據(jù)時(shí),Redis 需要挑選數(shù)據(jù)進(jìn)入「第一次淘汰時(shí)創(chuàng)建的候選集合」。

    挑選的標(biāo)準(zhǔn)是:能進(jìn)入候選集合的數(shù)據(jù)的 lru 字段值必須小于「候選集合中最小的 lru 值」。

    當(dāng)有新數(shù)據(jù)進(jìn)入備選數(shù)據(jù)集后,如果備選數(shù)據(jù)集中的數(shù)據(jù)個(gè)數(shù)達(dá)到了設(shè)置的閾值時(shí)。Redis 就把備選數(shù)據(jù)集中 lru 字段值最小的數(shù)據(jù)淘汰出去。

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

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

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

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

    整個(gè)流程相當(dāng)于隨機(jī)取 5 個(gè) key 放入 pool,然后淘汰 pool 中空閑時(shí)間最大的 key,然后再隨機(jī)取 5 個(gè) key放入 pool,繼續(xù)淘汰 pool 中空閑時(shí)間最大的 key,一直持續(xù)下去。

    在進(jìn)入淘汰前會(huì)計(jì)算出需要釋放的內(nèi)存大小,然后就一直循環(huán)上述流程,直至釋放足夠的內(nèi)存。

    LFU

    在一些場景下,有些數(shù)據(jù)被訪問的次數(shù)非常少,甚至只會(huì)被訪問一次。當(dāng)這些數(shù)據(jù)服務(wù)完訪問請(qǐng)求后,如果還繼續(xù)留存在緩存中的話,就只會(huì)白白占用內(nèi)存空間。這種情況,就是緩存污染。

    為了應(yīng)對(duì)緩存污染問題,Redis 從 4.0 版本開始增加了 LFU 淘汰策略。

    LFU 緩存策略是在 LRU 策略基礎(chǔ)上,為每個(gè)數(shù)據(jù)增加了一個(gè)「計(jì)數(shù)器」,來統(tǒng)計(jì)這個(gè)數(shù)據(jù)的訪問次數(shù)。

    LFU 篩選邏輯
    • 當(dāng)使用 LFU 策略篩選淘汰數(shù)據(jù)時(shí),首先會(huì)根據(jù)數(shù)據(jù)的訪問次數(shù)進(jìn)行篩選,把訪問次數(shù)最低的數(shù)據(jù)淘汰出緩存。
    • 如果兩個(gè)數(shù)據(jù)的訪問次數(shù)相同,LFU 策略再比較這兩個(gè)數(shù)據(jù)的訪問時(shí)效性,把距離上一次訪問時(shí)間更久的數(shù)據(jù)淘汰出緩存。
    LFU 的具體實(shí)現(xiàn)

    我們?cè)谇懊嬲f過,為了避免操作鏈表的開銷,Redis 在實(shí)現(xiàn) LRU 策略時(shí)使用了兩個(gè)近似方法:

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

    在此基礎(chǔ)上,Redis 在實(shí)現(xiàn) LFU 策略的時(shí)候,只是把原來 24bit 大小的 lru 字段,又進(jìn)一步拆分成了兩部分:

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

    但是我們會(huì)發(fā)現(xiàn)一個(gè)問題,counter 值的最大記錄值只有255。當(dāng)幾個(gè)緩存數(shù)據(jù)的 counter 值 都達(dá)到255值,就無法正確根據(jù)訪問次數(shù)來決定數(shù)據(jù)的淘汰了。

    所以Redis 針對(duì)這個(gè)問題進(jìn)行了優(yōu)化:在實(shí)現(xiàn) LFU 策略時(shí),Redis 并沒有采用數(shù)據(jù)每被訪問一次,就給對(duì)應(yīng)的 counter 值加 1 的計(jì)數(shù)規(guī)則,而是采用了一個(gè)更優(yōu)化的計(jì)數(shù)規(guī)則。

    Redis 對(duì) LFU 的實(shí)現(xiàn)

    Redis 實(shí)現(xiàn) LFU 策略時(shí)采用計(jì)數(shù)規(guī)則:

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

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

    為什么 baseval 的初始值是5,而不是0?是因?yàn)檫@樣可以避免數(shù)據(jù)剛被寫入緩存,就因?yàn)樵L問次數(shù)少而被立即淘汰。

    使用了這種計(jì)算規(guī)則后,我們可以通過設(shè)置不同的 lfu_log_factor 配置項(xiàng),來控制計(jì)數(shù)器值增加的速度,避免 counter 值很快就到 255 了。

    這張表是根據(jù)Redis官網(wǎng)獲得的,進(jìn)一步說明 LFU 策略計(jì)數(shù)器遞增的效果。
    它記錄了當(dāng) lfu_log_factor 取不同值時(shí),在不同的實(shí)際訪問次數(shù)情況下,計(jì)數(shù)器值的變化情況。

    lfu_log_factor100 hits1000 hits100K hits1M hits10M hits
    0104255255255255
    11849255255255
    101018142255255
    10081149143255

    通過上表的分析:

    • 當(dāng) lfu_log_factor 取值為 1 時(shí),實(shí)際訪問次數(shù)為 100K 后,counter 值就達(dá)到 255 了,無法再區(qū)分實(shí)際訪問次數(shù)更多的數(shù)據(jù)了。
    • 當(dāng) lfu_log_factor 取值為 100 時(shí),當(dāng)實(shí)際訪問次數(shù)為 10M 時(shí),counter 值才達(dá)到 255。

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

    從剛才的表中,我們可以看到,當(dāng) lfu_log_factor 取值為 10 時(shí),百、千、十萬級(jí)別的訪問次數(shù)對(duì)應(yīng)的 counter 值 已經(jīng)有明顯的區(qū)分了。所以,我們?cè)趹?yīng)用 LFU 策略時(shí),一般可以將 lfu_log_factor 取值為 10。

    但是對(duì)于一些業(yè)務(wù)場景,上方的設(shè)計(jì)會(huì)存在問題:比如說有些數(shù)據(jù)在「短時(shí)間內(nèi)被大量訪問后就不會(huì)再被訪問了」。

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

    為此,Redis 在實(shí)現(xiàn) LFU 策略時(shí),還設(shè)計(jì)了一個(gè)「 counter 值的衰減機(jī)制」。

    LFU 中的 counter 值的衰減機(jī)制

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

  • LFU 策略會(huì)計(jì)算當(dāng)前時(shí)間和數(shù)據(jù)最近一次訪問時(shí)間的差值,并把這個(gè)差值換算成以分鐘為單位。
  • 然后,LFU 策略再把這個(gè)差值除以 lfu_decay_time 值,所得的結(jié)果就是數(shù)據(jù) counter 要衰減的值。
  • 通過上方的第二點(diǎn),我們就能知道一個(gè)規(guī)律,lfu_decay_time 值越大,那么相應(yīng)的衰減值會(huì)變小,衰減效果也會(huì)減弱;反之相應(yīng)的衰減值會(huì)變大,衰減效果也會(huì)增強(qiáng)。

    所以,如果業(yè)務(wù)應(yīng)用中有短時(shí)高頻訪問的數(shù)據(jù)的話,建議把 lfu_decay_time 值設(shè)置為 1。

    使用總結(jié)

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

    Redis 事務(wù)相對(duì)于Mysql 事務(wù)來說較為簡單,大家可以將二者進(jìn)行對(duì)比,下文也會(huì)整理。

    概念

    Redis 事務(wù)的本質(zhì)是一組命令的集合。

    事務(wù)支持一次執(zhí)行多個(gè)命令,一個(gè)事務(wù)中所有命令都會(huì)被序列化。在事務(wù)執(zhí)行過程,會(huì)按照順序串行化執(zhí)行隊(duì)列中的命令,其他客戶端提交的命令請(qǐng)求不會(huì)插入到事務(wù)執(zhí)行命令序列中。

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

    它的主要作用就是串聯(lián)多個(gè)命令防止別的命令插隊(duì)。

    事務(wù)階段

    我們可以把Redis 事務(wù)的執(zhí)行分為三個(gè)階段:

  • 開始事務(wù)
  • 命令入隊(duì)
  • 執(zhí)行事務(wù)
  • 從輸入Multi命令開始,輸入的命令都會(huì)依次進(jìn)入命令隊(duì)列中,但不會(huì)執(zhí)行,直到輸入 Exec 后,Redis會(huì)將之前的命令隊(duì)列中的命令依次執(zhí)行。組隊(duì)的過程中可以通過 discard。

    事務(wù)錯(cuò)誤處理

    事務(wù)的錯(cuò)誤分為兩種情況:

    • 如果組隊(duì)中某個(gè)命令報(bào)出了錯(cuò)誤,執(zhí)行時(shí)整個(gè)的所有隊(duì)列都會(huì)被取消。
    • 如果執(zhí)行階段某個(gè)命令報(bào)出了錯(cuò)誤,則只有報(bào)錯(cuò)的命令不會(huì)被執(zhí)行,而其他的命令都會(huì)執(zhí)行不會(huì)回滾。

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

    Watch 監(jiān)控

    引入

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

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

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

    watch 命令

    在執(zhí)行 multi 之前,先執(zhí)行watch key1 [key2],可以監(jiān)視一個(gè)(或多個(gè)) key ,如果在事務(wù)執(zhí)行之前這個(gè)(或這些) key 被其他命令所改動(dòng),那么事務(wù)將被打斷

    舉例說明:

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

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

    我很生氣,因?yàn)槲页渲凳×?#xff0c;但是我去賬戶上一看,變成999元了,我馬上給自己一巴掌,“在生氣什么呢?”…

    模擬上方情景,這是控制臺(tái)1的操作:

    模擬上方情景,這是控制臺(tái)2的操作:

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

    unwatch 命令

    取消 WATCH 命令對(duì)所有 key 的監(jiān)視。

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

    總結(jié)說明

    redis 的事務(wù)不推薦在實(shí)際中使用,如果要使用事務(wù),推薦使用 Lua 腳本,redis 會(huì)保證一個(gè) Lua 腳本里的所有命令的原子性。

    Redis集群

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

    主從復(fù)制

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

    主從復(fù)制架構(gòu)

    主從復(fù)制,將 Redis 實(shí)例分為兩中角色,一種是被復(fù)制的服務(wù)器稱為主服務(wù)器(master),而對(duì)主服務(wù)器進(jìn)行復(fù)制的服務(wù)器被稱為從服務(wù)器(slave)。

    當(dāng)主數(shù)據(jù)庫有數(shù)據(jù)寫入,會(huì)將數(shù)據(jù)同步復(fù)制給從節(jié)點(diǎn),一個(gè)主數(shù)據(jù)庫可以同時(shí)擁有多個(gè)從數(shù)據(jù)庫,而從數(shù)據(jù)庫只能擁有一個(gè)主數(shù)據(jù)庫。值得一提的是,從節(jié)點(diǎn)也可以有從節(jié)點(diǎn),呈現(xiàn)級(jí)聯(lián)結(jié)構(gòu)。

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

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

    開啟主從復(fù)制方式

    命令

    通過slaveof 命令,將 127.0.0.1:6380 的redis實(shí)例成為 127.0.0.1:6379 的redis實(shí)例的從服務(wù)器:

    slaveof 127.0.0.1 6379

    測試如下:


    配置

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

    # 通用配置 # bind 127.0.0.1 # 綁定監(jiān)聽的網(wǎng)卡IP,注釋掉或配置成0.0.0.0可使任意IP均可訪問 port 6379 # 設(shè)置監(jiān)聽端口 #是否開啟保護(hù)模式,默認(rèn)開啟。 # 設(shè)置為no之后最好設(shè)置一下密碼 protected-mode no #是否在后臺(tái)執(zhí)行,yes:后臺(tái)運(yùn)行;no:不是后臺(tái)運(yùn)行 daemonize yes # 復(fù)制選項(xiàng),slave復(fù)制對(duì)應(yīng)的master。 # replicaof <masterip> <masterport> #如果master設(shè)置了requirepass,那么slave要連上master,需要有master的密碼才行。masterauth就是用來 # 配置master的密碼,這樣可以在連上master后進(jìn)行認(rèn)證。 # masterauth <master-password>

    在啟動(dòng)節(jié)點(diǎn)時(shí)輸入命令

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

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

    啟動(dòng)命令

    參考博客鏈接:redis啟動(dòng)命令及集群創(chuàng)建

    復(fù)制的實(shí)現(xiàn)【重點(diǎn)】

    1. 設(shè)置主服務(wù)器的地址和端口

    例如客戶端操作從服務(wù)器執(zhí)行如下命令:

    127.0.0.1> SLAVEOF 127.0.0.1 6379

    從服務(wù)器會(huì)將客戶端給定的主服務(wù)器IP地址以及端口號(hào)保存到當(dāng)前從服務(wù)器狀態(tài)的 masterhost 屬性和 masterport 屬性中。

    SLAVEOF 命令是一個(gè)異步命令,在完成屬性的設(shè)置工作后,從服務(wù)器會(huì)向客戶端返回"OK",之后開始執(zhí)行真正的復(fù)制工作。

    2. 建立套接字連接

    從服務(wù)器根據(jù)指定的 IP地址和端口號(hào),創(chuàng)建連向主服務(wù)器套接字(socket)連接。

    主服務(wù)器在接受(accept) 從服務(wù)器的套接字連接之后,為該套接字創(chuàng)建相應(yīng)的客戶端狀態(tài)。

    這個(gè)時(shí)候可以將從服務(wù)器理解為主服務(wù)器的客戶端。

    3. 發(fā)送 PING 命令

    從服務(wù)器主服務(wù)器發(fā)送一個(gè) PING 命令,以檢査套接字的讀寫狀態(tài)是否正常、 主服務(wù)器能否正常處理命令請(qǐng)求。

    從服務(wù)器在發(fā)送 PING 命令后,會(huì)遇到三種情況:

  • 主服務(wù)器響應(yīng)超時(shí),表示當(dāng)前兩者之間網(wǎng)絡(luò)連接狀態(tài)不佳,從服務(wù)器重新創(chuàng)建連向主服務(wù)器的套接字。
  • 主服務(wù)器返回錯(cuò)誤,表示主服務(wù)器暫時(shí)無法處理從服務(wù)器的命令請(qǐng)求,從服務(wù)器重新創(chuàng)建連向主服務(wù)器的套接字。
  • 主服務(wù)器返回 "PONG",表示主從之間網(wǎng)絡(luò)連接狀態(tài)正常,主服務(wù)器可以正常處理從服務(wù)器的命令請(qǐng)求。
  • 4. 身份驗(yàn)證

    存在這一步的前提是:從服務(wù)器設(shè)置了 masterauth 選項(xiàng),那么就要進(jìn)行這一步的身份驗(yàn)證,否則跳過。

    從服務(wù)器將 masterauth 選項(xiàng)的值封裝成AUTH password 命令并向主服務(wù)器發(fā)送來進(jìn)行身份驗(yàn)證。

    從服務(wù)器在身份驗(yàn)證階段可能會(huì)遇到以下幾種情況:

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

    從服務(wù)器主服務(wù)器發(fā)送當(dāng)前服務(wù)器的監(jiān)聽端口號(hào), 主服務(wù)器收到后記錄在從服務(wù)器所對(duì)應(yīng)的客戶端狀態(tài)的 slave_listening_port 屬性中。

    執(zhí)行命令為 REPLCONF listening-port <port-number> ,port-number 即為端口號(hào)。

    目前 slave_listening_port 唯一的作用就是在主服務(wù)器執(zhí)行 INFO replication 命令時(shí)打印從服務(wù)器端口號(hào)。

    6. 同步

    從服務(wù)器主服務(wù)器發(fā)送 PSYNC 命令,執(zhí)行同步操作,此時(shí)兩者互為客戶端。

    PSYNC 命令有兩種執(zhí)行情況:

  • 如果從服務(wù)器以前沒有復(fù)制過或者執(zhí)行過 slaveof no one 命令,那么從服務(wù)器在開始一次新的復(fù)制時(shí),會(huì)給主服務(wù)器發(fā)送 PSYNC ? -1 命令。主動(dòng)請(qǐng)求進(jìn)行完整重同步。
  • 相反,如果已經(jīng)復(fù)制過,那么從服務(wù)器在開始一次新的復(fù)制時(shí),將向主服務(wù)器發(fā)送 PSYNC <runid > <offset> 命令,runid 是上次主服務(wù)器的運(yùn)行ID,offset是從服務(wù)器的復(fù)制偏移量。
  • 主服務(wù)器返回從服務(wù)器也有三種情況:

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

    7. 命令傳播

    當(dāng)完成了同步之后,就會(huì)進(jìn)入命令傳播階段,這時(shí)主服務(wù)器只要一直將自己執(zhí)行的寫命令發(fā)送給從服務(wù)器,而從服務(wù)器只要一直接收并執(zhí)行主服務(wù)器發(fā)來的寫命令,就可以保證主從一致了。

    主從復(fù)制優(yōu)缺點(diǎn)

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

    總結(jié)

    其實(shí)redis的主從模式很簡單,在實(shí)際的生產(chǎn)環(huán)境中很少使用,不建議在實(shí)際的生產(chǎn)環(huán)境中使用主從模式來提供系統(tǒng)的高可用性,之所以不建議使用都是由它的缺點(diǎn)造成的,在數(shù)據(jù)量非常大的情況,或者對(duì)系統(tǒng)的高可用性要求很高的情況下,主從模式也是不穩(wěn)定的。雖然這個(gè)模式很簡單,但是這個(gè)模式是其他模式的基礎(chǔ),所以理解了這個(gè)模式,對(duì)其他模式的學(xué)習(xí)會(huì)很有幫助。

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

    哨兵模式

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

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

    哨兵模式架構(gòu)

    哨兵(Sentinel) 是 Redis 的高可用性解決方案:由一個(gè)或多個(gè) Sentinel 實(shí)例組成的 Sentinel 系統(tǒng)可以監(jiān)視任意多個(gè)主服務(wù)器,以及這些主服務(wù)器屬下的所有從服務(wù)器。

    Sentinel 可以在被監(jiān)視的主服務(wù)器進(jìn)入下線狀態(tài)時(shí),自動(dòng)將下線主服務(wù)器的某個(gè)從服務(wù)器升級(jí)為新的主服務(wù)器,然后由新的主服務(wù)器代替已下線的主服務(wù)器繼續(xù)處理命令請(qǐng)求。

    哨兵進(jìn)程

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

    哨兵進(jìn)程的作用
  • 監(jiān)控(Monitoring): 哨兵(sentinel) 會(huì)不斷地檢查你的Master和Slave是否運(yùn)作正常。
  • 提醒(Notification):當(dāng)被監(jiān)控的某個(gè)Redis節(jié)點(diǎn)出現(xiàn)問題時(shí), 哨兵(sentinel) 可以通過 API 向管理員或者其他應(yīng)用程序發(fā)送通知。
  • 自動(dòng)故障遷移(Automatic failover):當(dāng)一個(gè)Master不能正常工作時(shí),哨兵(sentinel) 會(huì)開始一次自動(dòng)故障遷移操作。
  • 哨兵(Sentinel) 和 一般Redis 的區(qū)別?

  • Sentinel 的本質(zhì)只是一個(gè)運(yùn)行在特殊模式下的 Redis 服務(wù)器。
  • 一般Redis 初始化時(shí)加載RDB 或者 AOF 文件還原數(shù)據(jù)庫狀態(tài),而Sentinel 不加載是因?yàn)樗皇褂脭?shù)據(jù)庫。
  • Sentinel 使用的代碼是 Sentinel專用代碼。
  • Sentinel 會(huì)初始化一個(gè) sentinel.c/sentinelState 結(jié)構(gòu),用于保存所有和 Sentinel 功能相關(guān)的狀態(tài),比如其中的 masters字典記錄了所有被 Sentinel 監(jiān)視的主服務(wù)器相關(guān)信息。
  • 哨兵的工作方式

    創(chuàng)建連接

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

    每個(gè)sentinel都會(huì)創(chuàng)建兩個(gè)連向主服務(wù)器的異步網(wǎng)絡(luò)連接

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

    為什么有兩個(gè)連接?

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

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

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

    獲取主服務(wù)器信息

    Sentinel 默認(rèn)會(huì)以10秒一次通過命令連接向被監(jiān)視的主服務(wù)器發(fā)送 INFO 命令,主服務(wù)器收到后回復(fù)自己的run_id、IP、端口、對(duì)應(yīng)的主服務(wù)器信息及主服務(wù)器下的所有從服務(wù)器信息。

    Sentinel 根據(jù)返回的主服務(wù)器信息更新自身的 *masters 實(shí)例結(jié)構(gòu);至于主服務(wù)器返回的從服務(wù)器信息用于更新對(duì)應(yīng)的slaves 字典列表。

    更新 slaves 字典時(shí)有兩種情況:

  • 如果存在從服務(wù)器對(duì)應(yīng)的實(shí)例結(jié)構(gòu),那么Sentinel會(huì)對(duì)該實(shí)例結(jié)構(gòu)進(jìn)行更新。
  • 如果不存在從服務(wù)器對(duì)應(yīng)的實(shí)例結(jié)構(gòu),會(huì)為這個(gè)從服務(wù)器新創(chuàng)建一個(gè)實(shí)例結(jié)構(gòu)。
  • 獲取從服務(wù)器信息

    Sentinel 同樣會(huì)和從服務(wù)器建立異步的命令連接和訂閱連接,并也會(huì)默認(rèn)10秒一次從服務(wù)器發(fā)送 INFO 命令,從服務(wù)器會(huì)回復(fù)自己的運(yùn)行run_id、角色role、從服務(wù)器復(fù)制偏移量offset、主服務(wù)器的ip和port、主從服務(wù)器連接狀態(tài)、從服務(wù)器優(yōu)先級(jí)等信息,sentinel會(huì)根據(jù)返回信息更新對(duì)應(yīng)的 slave 實(shí)例結(jié)構(gòu)。

    向主服務(wù)器和從服務(wù)器發(fā)送信息

    Sentinel 默認(rèn)會(huì)以2秒一次通過命令連接向所有被監(jiān)控的主服務(wù)器從服務(wù)器的_sentinel:hello頻道發(fā)送信息,信息的內(nèi)容包含兩種參數(shù):

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

    參數(shù)意義
    s_ipSentinel 的 IP地址
    s_portSentinel 的端口號(hào)
    s_runidSentinel 的運(yùn)行ID
    s_epochSentinel 當(dāng)前的配置紀(jì)元(configuration epoch)
    m_name主服務(wù)器的名字
    m_ip主服務(wù)器的IP地址
    m_port主服務(wù)器的端口號(hào)
    m_epoch主服務(wù)器當(dāng)前的配置紀(jì)元
    接收來自主服務(wù)器和從服務(wù)器的頻道信息

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

    _sentinel_:hello頻道 與 Sentinel 的關(guān)系是一對(duì)多的關(guān)系,作用在于發(fā)現(xiàn)多個(gè)監(jiān)控同一master的sentinel。

    在接收到其他 sentinel 發(fā)送的頻道信息后,會(huì)根據(jù)信息更新 master 對(duì)應(yīng)的 Sentinel 。

    與 master 數(shù)據(jù)結(jié)構(gòu)綁定后,會(huì)建立 Sentinel 與 Sentinel 的命令連接,為后續(xù)通訊做準(zhǔn)備。

    故障檢測

    檢測主觀下線

    Sentinel 默認(rèn)會(huì)以1秒一次的頻率向與它建立命令連接的所有實(shí)例(包括master、slave以及發(fā)現(xiàn)的其他sentinel)發(fā)送 PING 命令,對(duì)方接收后返回兩種回復(fù):

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

    在固定時(shí)間內(nèi),即 down-after-milliseconds(默認(rèn)單位為毫秒) 配置的時(shí)間內(nèi)收到的都是無效回復(fù),Sentinel 就會(huì)標(biāo)記 master 為主觀下線。與此同時(shí),Sentinel 會(huì)將 master 數(shù)據(jù)結(jié)構(gòu)中對(duì)應(yīng)的flags屬性更新為 SRI_S_DOWN 標(biāo)識(shí),表示被監(jiān)控的master在當(dāng)前sentinel中已經(jīng)進(jìn)入主觀下線狀態(tài)。

    down-after-milliseconds 的值,不僅是sentinel 用來判斷主服務(wù)器主觀下線狀態(tài),還用來判斷主服務(wù)器下所有從服務(wù)器,以及所有同樣監(jiān)視這個(gè)主服務(wù)器的其他Sentinel的主觀下線狀態(tài)。

    簡單說明,即 down-after-millsseconds 配置是作用于當(dāng)前sentinel所監(jiān)控的所有服務(wù)上的,也就是對(duì)應(yīng)master下的slave,以及其他sentinel。另外每個(gè)sentinel可以配置不同down-after-millsenconds,所以判定主觀下線的時(shí)間也就是不同的。

    檢測客觀下線

    判定 master 為主觀下線狀態(tài)的 Sentinel,通過命令詢問其他同樣監(jiān)控這一主服務(wù)器的 Sentinel,看它們是否認(rèn)為該 master 真的進(jìn)入了下線狀態(tài)。

    Sentinel 發(fā)送給其他 Sentinel 的命令為:

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

    參數(shù)說明:

    • Ip:被 Sentinel 判斷為主觀下線的主服務(wù)器的IP地址。
    • port:被 Sentinel 判斷為主觀下線的主服務(wù)器的端口號(hào)。
    • current_epoch:Sentinel 當(dāng)前的配置紀(jì)元,用于選舉領(lǐng)頭 Sentinel。
    • runid:可以是 * 符號(hào) 或 Sentinel 的 run_id,用 * 符號(hào)僅用于檢測主服務(wù)器的客觀下線狀態(tài);用Sentinel 的 run_id 是用于選舉領(lǐng)頭 Sentinel。

    其他 Sentinel 接收到 SEBTUBEL is-master-down-by-addr 命令后,會(huì)根據(jù)其中的主服務(wù)器IP和端口號(hào),檢查主服務(wù)器是否已下線,然后向源 Sentinel 返回一條包含三個(gè)參數(shù)的 Multi Bulk 回復(fù):

    <down_state> <leader_runid> <leader_epoch>

    參數(shù)說明:

    down_state:返回目標(biāo) Sentinel 對(duì)主服務(wù)器的檢查結(jié)果,1 代表已下線,0 代表為下線。

    leader_runid:可以是 * 符號(hào) 或 目標(biāo) Sentinel 的 run_id,用 * 符號(hào)僅用于檢測主服務(wù)器的下線狀態(tài);用局部領(lǐng)頭 Sentinel 的 run_id 是用于選舉局部領(lǐng)頭 Sentinel。

    leader_epoch:目標(biāo) Sentinel 的局部領(lǐng)頭 Sentinel 的配置紀(jì)元,用于選舉領(lǐng)頭 Sentinel。【僅在 leader_runid 的值不為 * 時(shí)有效,如果 leader_runid 的值為 *,則 leader_epoch 總為0】

    當(dāng) Sentinel 收到從其他 Sentinel 返回的足夠數(shù)量的已下線判斷之后,Sentinel會(huì)將主服務(wù)器實(shí)例結(jié)構(gòu)的 flags 屬性的 SRI_O_DOWN 標(biāo)識(shí)打開,表示主服務(wù)器已經(jīng)進(jìn)入客觀下線狀態(tài)。

    足夠數(shù)量的已下線判斷是多少呢?

    不同的 Sentinel 判斷客觀下線狀態(tài)的條件是不同的,具體不解釋了,看《Redis設(shè)計(jì)與實(shí)現(xiàn)》P238。

    選舉領(lǐng)頭 Sentinel

    當(dāng)一個(gè)主服務(wù)器被判斷為客觀下線時(shí),監(jiān)測這個(gè)下線主服務(wù)器的各個(gè) Sentinel 會(huì)進(jìn)行協(xié)商,選舉出一個(gè)領(lǐng)頭 Sentinel,并由領(lǐng)頭 Sentinel 對(duì)下線主服務(wù)器執(zhí)行故障轉(zhuǎn)移操作。

    下面盡量直白地介紹選舉領(lǐng)頭 Sentinel 的規(guī)則和方法:

    • 每個(gè)在線的 Sentinel 都有被選為領(lǐng)頭 Sentinel 的資格。

    • 同一個(gè)配置紀(jì)元內(nèi)(本質(zhì)是計(jì)數(shù)器,在每次選舉后自增一次),每個(gè) Sentinel 都有一次將某個(gè) Sentinel 設(shè)置為局部領(lǐng)頭 Sentinel 的機(jī)會(huì),并且設(shè)置后,在這個(gè)配置紀(jì)元里不能再更改。

    • 每個(gè)發(fā)現(xiàn)主服務(wù)器進(jìn)入客觀下線 的Sentinel 都會(huì)要求其他 Sentinel 將自己設(shè)置為局部領(lǐng)頭Sentinel。

    • 拉票方式為發(fā)送 SEBTUBEL is-master-down-by-addr 命令,剛才的 *號(hào)替換為源 Sentinel的 run_id,表示希望目標(biāo) Sentinel 設(shè)置自己為它的局部領(lǐng)頭 Sentinel。

    • 接收拉票命令的目標(biāo) Sentinel 可是非常單純,誰的命令先發(fā)給它,它就選誰當(dāng)自己的局部領(lǐng)頭 Sentinel,之后的拉票全部拒絕。

    • 當(dāng)然,既然目標(biāo) Sentinel根據(jù)先到先得確定了局部領(lǐng)頭 Sentinel,那也得和大家回個(gè)話,它會(huì)為發(fā)送拉票命令的源 Sentinel 回復(fù)命令,記錄了自身選擇的局部領(lǐng)頭 Sentinel的 run_id 和 配置紀(jì)元。

    • 如果某個(gè) Sentinel 被半數(shù)以上的 Sentinel 設(shè)置為了局部領(lǐng)頭 Sentinel,那么這個(gè)局部領(lǐng)頭sentinel就變成了領(lǐng)頭sentinel,同一個(gè)配置紀(jì)元內(nèi)可能會(huì)出現(xiàn)多個(gè)局部領(lǐng)頭sentinel,但是領(lǐng)頭sentinel只會(huì)產(chǎn)生一個(gè)。

    • 如果在給定的時(shí)限內(nèi),沒有任何一個(gè) Sentinel 被選舉為領(lǐng)頭 Sentinel,那么各個(gè) Sentinel 會(huì)在一段時(shí)間后再次選舉,直到選出領(lǐng)頭 Sentinel 為止。

    故障遷移

    在選舉出領(lǐng)頭 Sentinel 之后,領(lǐng)頭 Sentinel 會(huì)對(duì)已下線的主服務(wù)器執(zhí)行故障轉(zhuǎn)移操作,可分為三個(gè)步驟:

  • 在已下線的主服務(wù)器下的所有從服務(wù)器中,挑選一個(gè)從服務(wù)器作為新的主服務(wù)器。
  • 讓已下線的主服務(wù)器下的所有從服務(wù)器改為復(fù)制新的主服務(wù)器。
  • 將已下線的主服務(wù)器設(shè)置為新的主服務(wù)器的從服務(wù)器,當(dāng)它重新上線時(shí)會(huì)成為新的主服務(wù)器的從服務(wù)器。
  • 選出新的主服務(wù)器

    (一)、新的主服務(wù)器是從原主服務(wù)器下的從服務(wù)器中選擇的,所以需要選擇狀態(tài)良好、數(shù)據(jù)完整的從服務(wù)器。領(lǐng)頭 Sentinel 的數(shù)據(jù)結(jié)構(gòu)中保存了原master對(duì)應(yīng)的 slave ,Sentinel 會(huì)刪除狀態(tài)較差的slave。過濾執(zhí)行順序如下:

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

    (二)、現(xiàn)在過濾出的都是健康的從服務(wù)器了,然后 Sentinel 開始選擇新的主服務(wù)器,有以下三個(gè)優(yōu)先級(jí)順序:

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

    在發(fā)送 SLAVEOF no one 命令后,領(lǐng)頭 Sentinel 會(huì)以每秒一次的頻率(平時(shí)是十秒一次)向被選中的從服務(wù)器發(fā)送 INFO 命令,當(dāng)被升級(jí)的服務(wù)器的 role 字段從 slave 變?yōu)?master 時(shí),領(lǐng)頭 Sentinel 就知道它已經(jīng)順利成為新主服務(wù)器了。

    修改從服務(wù)器的復(fù)制目標(biāo)

    領(lǐng)頭 Sentinel 給已下線主服務(wù)器下的所有從服務(wù)器發(fā)送 SLAVEOF 命令,讓它們?nèi)?fù)制新的主服務(wù)器。

    將舊主服務(wù)器變?yōu)閺姆?wù)器

    因?yàn)榕f主服務(wù)器下線,領(lǐng)頭Sentinel 會(huì)修改它對(duì)應(yīng)主服務(wù)器下的實(shí)例結(jié)構(gòu)中的設(shè)置。

    等舊主服務(wù)器重新上線時(shí),Sentinel 就會(huì)向它發(fā)送 SLAVEOF 命令,讓他成為新的主服務(wù)器的從服務(wù)器。

    集群模式

    《Redis設(shè)計(jì)與實(shí)現(xiàn)》第十七章 集群 p245;

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

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

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

    集群模式架構(gòu)

    哨兵模式最大的缺點(diǎn)就是所有的數(shù)據(jù)都放在一臺(tái)服務(wù)器上,無法較好的進(jìn)行水平擴(kuò)展。

    為了解決哨兵模式的痛點(diǎn),集群模式應(yīng)運(yùn)而生。在高可用上,集群基本是直接復(fù)用的哨兵模式的邏輯,并且針對(duì)水平擴(kuò)展進(jìn)行了優(yōu)化。

    它具有的特點(diǎn)有:

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

    集群數(shù)據(jù)結(jié)構(gòu)

    使用 clusterNode 結(jié)構(gòu)保存一個(gè)節(jié)點(diǎn)的當(dāng)前狀態(tài),比如創(chuàng)建時(shí)間、名稱、配置紀(jì)元、IP、端口號(hào)等。

    每個(gè)節(jié)點(diǎn)都會(huì)為自己和集群中所有其他節(jié)點(diǎn)都創(chuàng)建一個(gè)對(duì)應(yīng)的 clusterNode 結(jié)構(gòu)來記錄各自的節(jié)點(diǎn)狀態(tài)。

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

    其中的 link 屬性是一個(gè) clusterLink 結(jié)構(gòu),該結(jié)構(gòu)保存連接節(jié)點(diǎn)所需的相關(guān)信息,包括套接字描述符、輸入緩沖區(qū)、輸出緩沖區(qū)。

    typedef struct clusterLink {// 連接的創(chuàng)建時(shí)間mestime_t ctime;// TCP 套接字描述符int fd;// 輸出緩沖區(qū),保存著待發(fā)送給其他節(jié)點(diǎn)的信息(message)sds sndbuf;// 輸入緩沖區(qū),保存著從其他節(jié)點(diǎn)接收到的信息sds rcvbuf;// 與這個(gè)連接相關(guān)聯(lián)的節(jié)點(diǎn),如果沒有的話就為 NULLstruct clusterNode *node; }

    最后一點(diǎn),每個(gè)節(jié)點(diǎn)都保存著一個(gè) clusterState 結(jié)構(gòu),這個(gè)結(jié)構(gòu)記錄了當(dāng)前節(jié)點(diǎn)視角下,所在集群目前所處的狀態(tài)。

    例如集群在線或下線狀態(tài)、包含節(jié)點(diǎn)個(gè)數(shù)、集群當(dāng)前的配置紀(jì)元等信息。

    typedef struct clsterState {// 指向當(dāng)前節(jié)點(diǎn)的指針clusterNode *myself;// 集群當(dāng)前的配置紀(jì)元,用于實(shí)現(xiàn)故障轉(zhuǎn)移uint64_t currentEpoch;// 集群當(dāng)前的狀態(tài),是在線還是下線int state;// 集群節(jié)點(diǎn)名單(包含myself節(jié)點(diǎn))// 字典的key是節(jié)點(diǎn)的名字,value是節(jié)點(diǎn)對(duì)應(yīng)的 clusterNode 結(jié)構(gòu)dict *nodes; }

    集群連接方式

    通過發(fā)送 CLUSTER MEET 命令,可以讓目標(biāo)節(jié)點(diǎn)A將另一個(gè)命令攜帶的節(jié)點(diǎn)B添加到目標(biāo)節(jié)點(diǎn)A當(dāng)前所在的集群中。

    CLUSTER MEET <ip> <port>

    收到命令后開始進(jìn)行節(jié)點(diǎn)A節(jié)點(diǎn)B握手階段,以此來確認(rèn)彼此的存在,為后面的通信打好基礎(chǔ),該過程簡單說明:

  • 客戶端向節(jié)點(diǎn)A發(fā)送 CLUSTER MEET 命令后,節(jié)點(diǎn)A向節(jié)點(diǎn)B發(fā)送 MEET 信息,給節(jié)點(diǎn)B創(chuàng)建 clusterNode 結(jié)構(gòu),并更新自己的 clusterState 結(jié)構(gòu)。
  • 節(jié)點(diǎn)B返回節(jié)點(diǎn)A PONG 信息。
  • 節(jié)點(diǎn)A返回節(jié)點(diǎn)B PING 信息。
  • 之后,節(jié)點(diǎn)A和節(jié)點(diǎn)B會(huì)通過Gossip 協(xié)議傳播給集群其他的節(jié)點(diǎn),讓他們也和節(jié)點(diǎn)B握手,最終整個(gè)集群達(dá)成共識(shí)。

    一般集群元數(shù)據(jù)的維護(hù)有兩種方式:集中式、Gossip 協(xié)議。在Redis集群中采用Gossip 協(xié)議進(jìn)行通信,所以說它是去中心化的集群。

    下面說一下這兩種方式的區(qū)別:

    集中式:是將集群元數(shù)據(jù)(節(jié)點(diǎn)信息、故障等等)幾種存儲(chǔ)在某個(gè)節(jié)點(diǎn)上。集中式元數(shù)據(jù)集中存儲(chǔ)的一個(gè)典型代表,就是大數(shù)據(jù)領(lǐng)域的 storm。它是分布式的大數(shù)據(jù)實(shí)時(shí)計(jì)算引擎,是集中式的元數(shù)據(jù)存儲(chǔ)的結(jié)構(gòu),底層基于 zookeeper(分布式協(xié)調(diào)的中間件)對(duì)所有元數(shù)據(jù)進(jìn)行存儲(chǔ)維護(hù)。

    gossip 協(xié)議所有節(jié)點(diǎn)都持有一份元數(shù)據(jù),不同的節(jié)點(diǎn)如果出現(xiàn)了元數(shù)據(jù)的變更,就不斷將元數(shù)據(jù)發(fā)送給其它的節(jié)點(diǎn),讓其它節(jié)點(diǎn)也進(jìn)行元數(shù)據(jù)的變更。

    集中式好處在于,元數(shù)據(jù)的讀取和更新,時(shí)效性非常好,一旦元數(shù)據(jù)出現(xiàn)了變更,就立即更新到集中式的存儲(chǔ)中,其它節(jié)點(diǎn)讀取的時(shí)候就可以感知到;不好在于,所有的元數(shù)據(jù)的更新壓力全部集中在一個(gè)地方,可能會(huì)導(dǎo)致元數(shù)據(jù)的存儲(chǔ)有壓力。

    gossip 協(xié)議好處在于,元數(shù)據(jù)的更新比較分散,不是集中在一個(gè)地方,更新請(qǐng)求會(huì)陸陸續(xù)續(xù)打到所有節(jié)點(diǎn)上去更新,降低了壓力;不好在于,元數(shù)據(jù)的更新有延時(shí),可能導(dǎo)致集群中的一些操作會(huì)有一些滯后。

    分布式尋址算法【引入】

    如果會(huì)的同學(xué)可以跳過,這里只做引申說明。

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

    • hash 算法(大量緩存重建)
    • 一致性 hash 算法(自動(dòng)緩存遷移)+ 虛擬節(jié)點(diǎn)(自動(dòng)負(fù)載均衡)
    • redis cluster 的 hash slot 算法
    hash 算法

    來了一個(gè) key,首先計(jì)算 hash 值,然后對(duì)節(jié)點(diǎn)數(shù)取模。然后打在不同的 master 節(jié)點(diǎn)上。一旦某一個(gè) master 節(jié)點(diǎn)宕機(jī),所有請(qǐng)求過來,都會(huì)基于最新的剩余 master 節(jié)點(diǎn)數(shù)去取模,嘗試去取數(shù)據(jù)。這會(huì)導(dǎo)致大部分的請(qǐng)求過來,全部無法拿到有效的緩存,導(dǎo)致大量的流量涌入數(shù)據(jù)庫。


    一致性 hash 算法

    一致性 hash 算法將整個(gè) hash 值空間組織成一個(gè)虛擬的圓環(huán),整個(gè)空間按順時(shí)針方向組織,下一步將各個(gè) master 節(jié)點(diǎn)(使用服務(wù)器的 ip 或主機(jī)名)進(jìn)行 hash。這樣就能確定每個(gè)節(jié)點(diǎn)在其哈希環(huán)上的位置。

    一致性 hash 算法也是使用取模的方法 hash算法的取模法是對(duì)服務(wù)器的數(shù)量進(jìn)行取模,而一致性 hash 算法是對(duì) **2^32 ** 取模:

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

    來了一個(gè) key,首先計(jì)算 hash 值,并確定此數(shù)據(jù)在環(huán)上的位置,從此位置沿環(huán)順時(shí)針“行走”,遇到的第一個(gè) master 節(jié)點(diǎn)就是 key 所在位置。

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

    hash 環(huán)數(shù)據(jù)傾斜 & 虛擬節(jié)點(diǎn)

    然而當(dāng)一致性 hash 算法在節(jié)點(diǎn)太少或是節(jié)點(diǎn)位置分布不均勻時(shí),容易造成大量請(qǐng)求都集中在某一個(gè)節(jié)點(diǎn)上,而造成緩存熱點(diǎn)的問題。如果i此時(shí)該熱點(diǎn)節(jié)點(diǎn)出現(xiàn)故障,那么失效緩存的數(shù)量也將達(dá)到最大值,在極端情況下,有可能引起系統(tǒng)的崩潰,這種情況被稱之為 數(shù)據(jù)傾斜。

    為了預(yù)防 數(shù)據(jù)傾斜 的問題,一致性 hash 算法引入了虛擬節(jié)點(diǎn)機(jī)制,即對(duì)每一個(gè)節(jié)點(diǎn)計(jì)算多個(gè) hash,每個(gè)計(jì)算結(jié)果位置都放置一個(gè)虛擬節(jié)點(diǎn)。這樣就實(shí)現(xiàn)了數(shù)據(jù)的均勻分布,負(fù)載均衡。

    具體說明,每一個(gè)服務(wù)節(jié)點(diǎn)計(jì)算多個(gè)哈希,每個(gè)計(jì)算結(jié)果位置都放置一個(gè)此服務(wù)節(jié)點(diǎn)。具體做法可以在服務(wù)器ip或主機(jī)名的后面增加編號(hào)來實(shí)現(xiàn)??梢詾槊颗_(tái)服務(wù)器計(jì)算三個(gè)虛擬節(jié)點(diǎn),于是可以分別計(jì)算 “Node1#1”、“Node1#2”、“Node1#3”、“Node2#1”、“Node2#2”、“Node2#3”的哈希值,這樣可以讓hash 環(huán)中存在多個(gè)節(jié)點(diǎn),使節(jié)點(diǎn)的分布更均勻,當(dāng)然可以虛擬出更多的虛擬節(jié)點(diǎn),以便減小hash環(huán)偏斜所帶來的影響,虛擬節(jié)點(diǎn)越多,hash環(huán)上的節(jié)點(diǎn)就越多,緩存被均勻分布的概率就越大。

    圖就不畫了…理解理解TAT

    hash slot 算法

    redis 集群采用數(shù)據(jù)分片的哈希槽來進(jìn)行數(shù)據(jù)存儲(chǔ)和數(shù)據(jù)的讀取。

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

    redis 集群中每個(gè) master 都會(huì)被指派部分的槽(slot),假如說當(dāng)前集群中有3個(gè)節(jié)點(diǎn)服務(wù)器,可能是這樣分配的 [0,5000]、[5001,10000]、[10001,16383]。

    槽位的實(shí)現(xiàn)其實(shí)就是一個(gè)長度為 16384 的二進(jìn)制數(shù)組,根據(jù)指定索引位上的二進(jìn)制位值來判斷節(jié)點(diǎn)是否處理指定索引的槽位。

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

  • 增加一個(gè) master,就將其他 master 的槽位移動(dòng)部分過去。
  • 減少一個(gè) master,就將它的槽位移動(dòng)到其他 master 上去。
  • 移動(dòng)槽位的成本是非常低的??蛻舳说?api,可以對(duì)指定的數(shù)據(jù),讓他們走同一個(gè)槽位,通過 hash tag 來實(shí)現(xiàn)。

    在Redis中通過 CLUSTER ADDSLOTS 命令來指派負(fù)責(zé)的槽位,后面會(huì)詳細(xì)說明。

    每個(gè)節(jié)點(diǎn)都會(huì)記錄哪些槽指派給了自己,哪些槽指派給了其他節(jié)點(diǎn)??蛻舳讼蚬?jié)點(diǎn)發(fā)送鍵命令,節(jié)點(diǎn)要計(jì)算這個(gè)鍵屬于哪個(gè)槽。如果是自己負(fù)責(zé)這個(gè)槽,那么直接執(zhí)行命令,如果不是,向客戶端返回一個(gè) MOVED 錯(cuò)誤,指引客戶端轉(zhuǎn)向正確的節(jié)點(diǎn)。

    任何一臺(tái)機(jī)器宕機(jī),另外兩個(gè)節(jié)點(diǎn),不影響的。因?yàn)?key 找的是 hash slot,不是機(jī)器。

    架構(gòu)圖參照上方《集群模式架構(gòu)》中。

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

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

    一致性 hash 算法 和 hash slot 算法的區(qū)別?
    定位規(guī)則區(qū)別

    它并不是閉合的,key的定位規(guī)則是根據(jù) CRC-16(key) % 16384 的值來判斷屬于哪個(gè)槽區(qū),從而判斷該key屬于哪個(gè)節(jié)點(diǎn),而一致性 hash 算法是根據(jù) hash(key) 的值來順時(shí)針找第一個(gè) hash(ip或主機(jī)名) 的節(jié)點(diǎn),從而確定key存儲(chǔ)在哪個(gè)節(jié)點(diǎn)。

    應(yīng)對(duì)熱點(diǎn)緩存區(qū)別

    一致性 hash 算法是創(chuàng)建虛擬節(jié)點(diǎn)來實(shí)現(xiàn)節(jié)點(diǎn)宕機(jī)后的數(shù)據(jù)轉(zhuǎn)移并保證數(shù)據(jù)的安全性和集群的可用性的。

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

    擴(kuò)容和縮容區(qū)別

    一致性 hash 算法在新增和刪除節(jié)點(diǎn)后,數(shù)據(jù)會(huì)按照順時(shí)針自動(dòng)來重新分布節(jié)點(diǎn)。

    redis 集群的新增和刪除節(jié)點(diǎn)都需要手動(dòng)來分配槽區(qū)

    集群的槽指派

    Redis集群通過分片來保存數(shù)據(jù)庫的鍵值對(duì):集群整個(gè)數(shù)據(jù)庫被分為16384個(gè)槽(slot),數(shù)據(jù)庫的每個(gè)鍵都屬于這16384個(gè)槽其中的一個(gè),集群中的每個(gè)節(jié)點(diǎn)可以處理0個(gè)到16384個(gè)槽。

    指派節(jié)點(diǎn)槽信息

    當(dāng)集群使用 CLUSTER MEET 命令,整個(gè)集群仍處于下線狀態(tài),此時(shí)必須通過它們指派槽,通過發(fā)送 CLUSTER ADDSLOTS 命令給節(jié)點(diǎn),將一個(gè)或多個(gè)槽指派給節(jié)點(diǎn)負(fù)責(zé):

    CLUSTER ADDSLOTS <slot> [slot...]

    比如說將 0 到 5000 個(gè)槽指派給節(jié)點(diǎn)7000負(fù)責(zé):

    CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000

    然后以此類推給其他節(jié)點(diǎn)指派槽。

    槽位是在 clusterNode 結(jié)構(gòu)中的 slots 屬性和 numslot 屬性記錄的,記錄當(dāng)前節(jié)點(diǎn)負(fù)責(zé)處理哪些槽:

    struct clusterNode {//...// 二進(jìn)制位數(shù)組unsigned char slots[16384/8];// 記錄節(jié)點(diǎn)負(fù)責(zé)處理的槽的數(shù)量,即slots數(shù)組中值為1的二進(jìn)制位的數(shù)量int numslots; }

    在上面小節(jié)《分布式尋址算法》的《hash slot 算法》中說過,槽的本質(zhì)就是一個(gè)二進(jìn)制位數(shù)組,通過對(duì)[0,16383]上的對(duì)應(yīng)索引為標(biāo)記來判斷是否處理該槽位:如果slots數(shù)組上在指定索引位的二進(jìn)制位的值為1,標(biāo)識(shí)節(jié)點(diǎn)負(fù)責(zé)處理該槽,反之同理。

    CLUSTER ADDSLOTS 的命令實(shí)現(xiàn)

    CLUSTER ADDSLOTS 命令的實(shí)現(xiàn)也比較簡單:

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

    傳播節(jié)點(diǎn)槽信息

    節(jié)點(diǎn)會(huì)將自己的 slots 數(shù)組通過消息發(fā)送給集群中的其他節(jié)點(diǎn),告知它們自己目前負(fù)責(zé)的槽位。

    當(dāng)其他節(jié)點(diǎn)接收到消息,會(huì)更新自己的在 clusterState.nodes 字典中對(duì)應(yīng)節(jié)點(diǎn)的 clusterNode 結(jié)構(gòu)中的 slots 數(shù)組。

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

    在 clusterState 結(jié)構(gòu)中的 slots 數(shù)組記錄了集群中所有 16384 個(gè)槽的指派信息:

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

    slots 數(shù)組包含 16384 個(gè)項(xiàng),每個(gè)數(shù)組項(xiàng)都是一個(gè)指向 clusterNode 的指針:對(duì)應(yīng)指針指向 NULL 時(shí),說明還未分配;指向 clusterNode 結(jié)構(gòu)時(shí),說明已經(jīng)指派給了對(duì)應(yīng)結(jié)構(gòu)所代表的節(jié)點(diǎn)。

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

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

    • 如果只使用 clusterNode.slots來記錄,每次都需要遍歷所有 clusterNode 結(jié)構(gòu),復(fù)雜度為O(N)。
    • 但如果使用 clusterState.slots 來記錄,只需要訪問 clusterState.slots對(duì)應(yīng)的索引位即可,復(fù)雜度為O(1)。

    集群執(zhí)行命令

    建立集群,并且分配完槽位,此時(shí)集群就會(huì)進(jìn)入上線狀態(tài),這時(shí)候客戶端就可以向集群中的節(jié)點(diǎn)發(fā)送數(shù)據(jù)指令了。

    客戶端在向節(jié)點(diǎn)發(fā)送與數(shù)據(jù)庫鍵有關(guān)的命令時(shí),接收命令的節(jié)點(diǎn)就會(huì)計(jì)算出命令要處理的數(shù)據(jù)庫鍵屬于哪個(gè)槽,并檢查這個(gè)槽是否指派個(gè)了自己:

    • 如果鍵所在的槽正好指派給當(dāng)前節(jié)點(diǎn),那么節(jié)點(diǎn)就直接執(zhí)行這個(gè)命令
    • 如果鍵所在的槽沒有指派給當(dāng)前節(jié)點(diǎn),那么節(jié)點(diǎn)就會(huì)向客戶端返回 MOVED 錯(cuò)誤指引客戶端向正確的節(jié)點(diǎn),并再次發(fā)送之前想要執(zhí)行的命令。

    節(jié)點(diǎn)會(huì)使用以下算法來給指定 key 進(jìn)行計(jì)算:

    def slot_number(key):return CRC16(key) & 16383
    • CRC16(key):計(jì)算鍵 key 的 CRC-16 校驗(yàn)和。
    • & 16383:計(jì)算出介于0至16383之間的整數(shù)作為鍵 key 的槽號(hào)。

    當(dāng)節(jié)點(diǎn)計(jì)算出鍵所屬的槽后,節(jié)點(diǎn)會(huì)檢查自己 clusterState.slots 數(shù)組中的指定槽位,判斷是否由自己負(fù)責(zé):

    • 如果 clusterState.slot[i] 等于 clusterState.myself,說明是由當(dāng)前節(jié)點(diǎn)負(fù)責(zé)的。
    • 如果 clusterState.slot[i] 不等于 clusterState.myself,說明不是由當(dāng)前節(jié)點(diǎn)負(fù)責(zé)的,會(huì)根據(jù) clusterState.slot[i] 指向的 clusterNode 結(jié)構(gòu)中所記錄的 IP 和 端口號(hào),返回客戶端 MOVED 錯(cuò)誤,指引客戶端轉(zhuǎn)向正在處理該槽的節(jié)點(diǎn)。
    MOVED 錯(cuò)誤

    MOVED 錯(cuò)誤的格式為:

    MOVED <slot> <ip>:<port>
    • slot:鍵所在的槽。
    • ip:port:負(fù)責(zé)處理該槽節(jié)點(diǎn)的IP地址和端口號(hào)。

    MOVED 錯(cuò)誤一般是不會(huì)打印的,而是根據(jù)該錯(cuò)誤自動(dòng)進(jìn)行節(jié)點(diǎn)轉(zhuǎn)向,并打印轉(zhuǎn)向信息。

    如果在單機(jī) redis 的情況下,是會(huì)被客戶端打印出來的。

    節(jié)點(diǎn)數(shù)據(jù)庫的實(shí)現(xiàn)

    節(jié)點(diǎn)只能使用0號(hào)數(shù)據(jù)庫,而單機(jī)Redis服務(wù)器則沒有限制

    節(jié)點(diǎn)除了將鍵值對(duì)保存在數(shù)據(jù)庫中之外,還會(huì)用 clusterState 結(jié)構(gòu)中的 slots_to_keys跳躍表來保存槽和鍵之間的關(guān)系:

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

    slots_to_keys 跳表中每個(gè)節(jié)點(diǎn)的分值(score)都是一個(gè)槽位號(hào);每個(gè)節(jié)點(diǎn)的成員(member)都是一個(gè)數(shù)據(jù)庫鍵。

    • 當(dāng)節(jié)點(diǎn)往數(shù)據(jù)庫中添加新的鍵值對(duì)時(shí),節(jié)點(diǎn)會(huì)將鍵的槽位號(hào)以及這個(gè)鍵關(guān)聯(lián)到 slot_to_keys 跳表中。
    • 當(dāng)節(jié)點(diǎn)刪除數(shù)據(jù)庫中的某個(gè)鍵值對(duì)時(shí),節(jié)點(diǎn)就會(huì)在 slot_to_keys跳表中解除它們的關(guān)聯(lián)關(guān)系。

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

    Redis 集群的重新分片操作可以將任意數(shù)量已經(jīng)指派給某個(gè)節(jié)點(diǎn)的槽改為指派給另一個(gè)節(jié)點(diǎn),并且相關(guān)聯(lián)槽位的鍵值對(duì)也會(huì)從源節(jié)點(diǎn)移動(dòng)到目標(biāo)節(jié)點(diǎn)。

    重新分片的操作是可以在線進(jìn)行的,保證了高可用。

    我們就以在線擴(kuò)容節(jié)點(diǎn)的情況來說吧:比如現(xiàn)在準(zhǔn)備在集群中增加一個(gè)節(jié)點(diǎn),如何將原有分片中的若干個(gè)槽位指派給新添加的節(jié)點(diǎn)?

    Redis 集群的重新分片操作是由 Redis 集群管理軟件 redis-trib 負(fù)責(zé)執(zhí)行的:Redis 提供重新分配的所有命令,而 redis-trib 通過向源節(jié)點(diǎn)和目標(biāo)接待你發(fā)送命令來進(jìn)行重新分片操作。

    redis-trib 對(duì)集群的單個(gè)槽進(jìn)行重新分片的步驟如下:

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

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

    在重新分片操作期間,可能會(huì)出現(xiàn)一部分鍵值對(duì)被遷出,一部分鍵值還未被遷出,即在源節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn)都由對(duì)應(yīng)槽的數(shù)據(jù)。

    當(dāng)節(jié)點(diǎn)向源節(jié)點(diǎn)發(fā)送一個(gè)與數(shù)據(jù)庫鍵相關(guān)的命令,并且該鍵的槽位正好處在重新分片的過程中:

  • 源節(jié)點(diǎn)現(xiàn)在自己的庫中找指定鍵。
  • 找到的話,直接執(zhí)行客戶端發(fā)送的命令。
  • 沒找到的話,判斷當(dāng)前源節(jié)點(diǎn)是否正在遷移對(duì)應(yīng)數(shù)據(jù)庫鍵所在的槽位。
  • 如果沒有在遷移,說明鍵不存在,正常執(zhí)行命令。
  • 如果在遷移,說明鍵有可能在目標(biāo)節(jié)點(diǎn),返回 ASK 錯(cuò)誤。
  • ASK 錯(cuò)誤同 MOVED 錯(cuò)誤類似,也是不會(huì)打印的,也會(huì)根據(jù)錯(cuò)誤提供的 IP 和 端口號(hào)自動(dòng)進(jìn)行轉(zhuǎn)向操作。

    同理,單機(jī)模式下會(huì)打印錯(cuò)誤。

    那 ASK 錯(cuò)誤 和 MOVED 錯(cuò)誤有什么區(qū)別呢?

    雖然它們能導(dǎo)致客戶端轉(zhuǎn)向,但是 MOVED 錯(cuò)誤代表槽的負(fù)責(zé)權(quán)已經(jīng)交給另一個(gè)節(jié)點(diǎn)了;而 ASK 錯(cuò)誤只是兩個(gè)節(jié)點(diǎn)在遷移槽的過程中使用的臨時(shí)措施。

    CLUSTER SETSLOT IMPORTING 命令的實(shí)現(xiàn)

    clusterState 結(jié)構(gòu)的 importing_slots_from 數(shù)組記錄了當(dāng)前節(jié)點(diǎn)正在從其他節(jié)點(diǎn)導(dǎo)入的槽

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

    如果 importing_slots_from[i] 的值不為 NULL,而是指向一個(gè) clusterNode 結(jié)構(gòu),那么表示當(dāng)前節(jié)點(diǎn)正在從 clusterNode 所代表的節(jié)點(diǎn)導(dǎo)入該槽。

    在對(duì)集群重新分片的時(shí)候,向目標(biāo)節(jié)點(diǎn)發(fā)送 CLUSTER SETSLOT IMPORTING 命令:

    CLUSTER SETSLOT <slot> IMPORTING <source_id>

    可以將目標(biāo)節(jié)點(diǎn) clusterState.importing_slots_from[i] 的值設(shè)置為 source_id所代表的節(jié)點(diǎn)的 clusterNode 結(jié)構(gòu)。

    CLUSTER SETSLOT MIGRATING 命令的實(shí)現(xiàn)

    clusterState 結(jié)構(gòu)的 migrating_slots_to 數(shù)組記錄了當(dāng)前節(jié)點(diǎn)正在遷移至其他節(jié)點(diǎn)的槽

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

    如果 migrating_slots_to[i] 的值不為 NULL,而是指向一個(gè) clusterNode 結(jié)構(gòu),那么表示當(dāng)前節(jié)點(diǎn)正在將該槽遷移到 clusterNode 所代表的節(jié)點(diǎn)。

    ASKING 命令

    當(dāng)客戶端接收到 ASK 錯(cuò)誤并轉(zhuǎn)向正在導(dǎo)入槽的節(jié)點(diǎn)時(shí),客戶端會(huì)先向節(jié)點(diǎn)發(fā)送一個(gè) ASKING 命令,然后才重新發(fā)送要執(zhí)行的命令,這是因?yàn)榭蛻舳巳绻话l(fā)送 ASKING 命令,而直接發(fā)送想要執(zhí)行的命令的話,那么客戶端發(fā)送的命令會(huì)被節(jié)點(diǎn)拒絕執(zhí)行,并返回 MOVED 錯(cuò)誤。

    復(fù)制和故障轉(zhuǎn)移

    Redis 集群中節(jié)點(diǎn)可分為主節(jié)點(diǎn)(master)和從節(jié)點(diǎn)(slave)。

    主節(jié)點(diǎn)用于處理槽;從節(jié)點(diǎn)用于復(fù)制某個(gè)主節(jié)點(diǎn),并在主節(jié)點(diǎn)下線時(shí),代替下線主節(jié)點(diǎn)繼續(xù)處理命令請(qǐng)求。

    設(shè)置從節(jié)點(diǎn)方式

    向一個(gè)節(jié)點(diǎn)發(fā)送命令:

    CLUSTER REPLICATE <node_id>

    可以讓接收命令的節(jié)點(diǎn)成為 node_id 所指定的節(jié)點(diǎn)的從節(jié)點(diǎn),并開始對(duì)主節(jié)點(diǎn)進(jìn)行復(fù)制操作,具體步驟如下:

  • 接收命令的節(jié)點(diǎn)首先找到 clusterState.nodes 字典中對(duì)應(yīng) node_id 所對(duì)應(yīng)節(jié)點(diǎn)的 clusterNode 結(jié)構(gòu),并將自身的 clusterState.myself.slaveof 指針指向這個(gè)結(jié)構(gòu),來記錄正在復(fù)制的主節(jié)點(diǎn)。
  • 修改自身 clusterState,myself.flags 屬性,關(guān)閉原來的 REDIS_NODE_MASTER 標(biāo)識(shí),打開 REDIS_NODE_SLAVE 標(biāo)識(shí),表明該節(jié)點(diǎn)已經(jīng)從主節(jié)點(diǎn)變成從節(jié)點(diǎn)。
  • 最后,節(jié)點(diǎn)會(huì)調(diào)用復(fù)制代碼對(duì)主節(jié)點(diǎn)進(jìn)行復(fù)制,相當(dāng)于向從節(jié)點(diǎn)發(fā)送 SLAVEOF 命令。
  • 故障檢測

    集群中每個(gè)節(jié)點(diǎn)都會(huì)定期向其他節(jié)點(diǎn)發(fā)送 PING 信息,以此檢測對(duì)方是否在線,如果接收 PING 信息的節(jié)點(diǎn)沒有在規(guī)定時(shí)間內(nèi)返回 PONG 信息,那么發(fā)送消息的節(jié)點(diǎn)會(huì)將接收消息的節(jié)點(diǎn)標(biāo)記為疑似下線(PFALL)。

    如果在集群中,半數(shù)以上負(fù)責(zé)槽的主節(jié)點(diǎn)都將某個(gè)主節(jié)點(diǎn)標(biāo)記為疑似下線,那么這個(gè)主節(jié)點(diǎn)就會(huì)被標(biāo)記為已下線(FALL)。

    將該主節(jié)點(diǎn)標(biāo)記為已下線的節(jié)點(diǎn)會(huì)向集群廣播關(guān)于該節(jié)點(diǎn)的 FALL 消息,所有收到這條 FALL 信息的節(jié)點(diǎn)都會(huì)立即將該節(jié)點(diǎn)標(biāo)記為已下線。

    故障轉(zhuǎn)移

    當(dāng)一個(gè)從節(jié)點(diǎn)發(fā)現(xiàn)自己正在復(fù)制的主節(jié)點(diǎn)進(jìn)入了下線狀態(tài)時(shí),從節(jié)點(diǎn)會(huì)對(duì)下線主節(jié)點(diǎn)進(jìn)行故障轉(zhuǎn)移,按照以下的執(zhí)行步驟:

  • 從下線主節(jié)點(diǎn)的所有從節(jié)點(diǎn)中選出一個(gè)從節(jié)點(diǎn),讓被選中的從節(jié)點(diǎn)執(zhí)行 SLAVE no one 命令,成為新的主節(jié)點(diǎn)。
  • 新的主節(jié)點(diǎn)會(huì)撤銷所有已下線主節(jié)點(diǎn)的槽指派,并將這些槽全部指派給自己
  • 新的主節(jié)點(diǎn)向集群廣播 PONG 信息,這條信息可以啊讓其他主節(jié)點(diǎn)直到這個(gè)節(jié)點(diǎn)已經(jīng)成為主節(jié)點(diǎn),并且接管了所有已下線的主節(jié)點(diǎn)負(fù)責(zé)處理的槽。
  • 新的主節(jié)點(diǎn)開始接收自己負(fù)責(zé)處理的槽相關(guān)的命令請(qǐng)求,故障轉(zhuǎn)移完成。
  • 選舉新的主節(jié)點(diǎn)過程

    新的主節(jié)點(diǎn)也是通過選舉產(chǎn)生的,簡單介紹一下它的選舉過程:

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

    有的小伙伴可能覺得 領(lǐng)頭Sentinel 的選舉不算 Raft,因?yàn)樗詈笫峭ㄟ^領(lǐng)頭 Sentinel 來控制故障遷移的具體過程,這個(gè)就是仁者見仁智者見智了。

    Raft 算法的實(shí)現(xiàn)可以參考一下Nacos 源碼中 RaftCore 類的實(shí)現(xiàn),比較通俗易懂。有時(shí)間我會(huì)發(fā)一下 Nacos 源碼中Raft選舉的實(shí)現(xiàn)。

    Redis應(yīng)用

    Redis 分布式鎖

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

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

    引入

    為什么需要分布式鎖?

    我們?cè)陂_發(fā)項(xiàng)目時(shí),如果需要在同進(jìn)程內(nèi)的不同線程并發(fā)訪問某項(xiàng)資源,可以使用各種互斥鎖、讀寫鎖。

    如果一臺(tái)主機(jī)上的多個(gè)進(jìn)程需要并發(fā)訪問某項(xiàng)資源,則可以使用進(jìn)程間同步的原語,例如信號(hào)量、管道、共享內(nèi)存等。

    但如果多臺(tái)主機(jī)需要同時(shí)訪問某項(xiàng)資源,就需要使用一種在全局可見并具有互斥性的鎖了。

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

    什么時(shí)候用分布式鎖?

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

    • 效率:使用分布式鎖可以避免不同節(jié)點(diǎn)重復(fù)相同的工作,這些工作會(huì)浪費(fèi)資源。比如用戶注冊(cè)后調(diào)用發(fā)送郵箱的接口發(fā)送通知,可能不同節(jié)點(diǎn)會(huì)發(fā)出多封郵箱
    • 安全:加分布式鎖同樣可以避免破壞正確性的發(fā)生,如果兩個(gè)節(jié)點(diǎn)在同一條數(shù)據(jù)上面操作,比如多個(gè)節(jié)點(diǎn)機(jī)器對(duì)同一個(gè)訂單操作不同的流程有可能會(huì)導(dǎo)致該筆訂單最后狀態(tài)出現(xiàn)錯(cuò)誤,造成損失。
    分布式鎖需要哪些特性呢?

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

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

    加鎖

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

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

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

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

    其中,NX 參數(shù)用于保證在多個(gè)線程并發(fā) set 下,只會(huì)有1個(gè)線程成功,起到了鎖的“唯一”性。

    舉例:

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

    解鎖

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

    一般解鎖需要兩步操作:

  • 查詢當(dāng)前“鎖”是否還是我們持有,因?yàn)榇嬖谶^期時(shí)間,所以可能等你想解鎖的時(shí)候,“鎖”已經(jīng)到期,然后被其他線程獲取了,所以我們?cè)诮怄i前需要先判斷自己是否還持有“鎖”。

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

  • 由于當(dāng)前 Redis 還沒有原子命令直接支持這兩步操作,所以當(dāng)前通常是使用 Lua 腳本來執(zhí)行解鎖操作,Redis 會(huì)保證腳本里的內(nèi)容執(zhí)行是一個(gè)原子操作。

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

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

    參數(shù)說明如下:

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

    續(xù)鎖

    一般為了防止死鎖,比如服務(wù)器宕機(jī)或斷線的情況下無法手動(dòng)解鎖,此時(shí)就需要給分布式鎖加上過期時(shí)間。

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

    首先,我們?cè)?strong>設(shè)置過期時(shí)間時(shí)要結(jié)合業(yè)務(wù)場景去設(shè)計(jì),盡量設(shè)置一個(gè)比較合理的值,就是理論上正常處理的話,在這個(gè)過期時(shí)間內(nèi)是一定能處理完畢的。

    然后我們需要應(yīng)對(duì)一些特殊惡劣情況進(jìn)行設(shè)計(jì)。

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

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

    守護(hù)線程“續(xù)命”存在的問題

    Redisson 使用看門狗(守護(hù)線程)“續(xù)命”的方案在大多數(shù)場景下是挺不錯(cuò)的,也被廣泛應(yīng)用于生產(chǎn)環(huán)境,但是在極端情況下還是會(huì)存在問題。

    問題例子如下:

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

    當(dāng)前比較主流的解法和思路有兩種:

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

    RedLock

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

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

    RedLock 算法

    算法很易懂,起 5 個(gè) master 節(jié)點(diǎn),分布在不同的機(jī)房盡量保證可用性。為了獲得鎖,client 會(huì)進(jìn)行如下操作:

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

    如果一個(gè) client 申請(qǐng)鎖失敗了,那么它需要稍等一會(huì)在重試避免多個(gè) client 同時(shí)申請(qǐng)鎖的情況,最好的情況是一個(gè) client 需要幾乎同時(shí)向 5 個(gè) master 發(fā)起鎖申請(qǐng)。另外就是如果 client 申請(qǐng)鎖失敗了它需要盡快在它曾經(jīng)申請(qǐng)到鎖的 master 上執(zhí)行 unlock 操作,便于其他 client 獲得這把鎖,避免這些鎖過期造成的時(shí)間浪費(fèi),當(dāng)然如果這時(shí)候網(wǎng)絡(luò)分區(qū)使得 client 無法聯(lián)系上這些 master,那么這種浪費(fèi)就是不得不付出的代價(jià)了。

    RedLock 的問題
    • 占用的資源過多,為了實(shí)現(xiàn)紅鎖,需要?jiǎng)?chuàng)建多個(gè)互不相關(guān)的云Redis實(shí)例或者自建Redis,成本較高。
    • 嚴(yán)重依賴系統(tǒng)時(shí)鐘。如果線程1從3個(gè)實(shí)例獲取到了鎖,但是這3個(gè)實(shí)例中的某個(gè)實(shí)例的系統(tǒng)時(shí)間走的稍微快一點(diǎn),則它持有的鎖會(huì)提前過期被釋放,當(dāng)他釋放后,此時(shí)又有3個(gè)實(shí)例是空閑的,則線程2也可以獲取到鎖,則可能出現(xiàn)兩個(gè)線程同時(shí)持有鎖了。
    • 如果線程1從3個(gè)實(shí)例獲取到了鎖,但是萬一其中有1臺(tái)重啟了,則此時(shí)又有3個(gè)實(shí)例是空閑的,則線程2也可以獲取到鎖,此時(shí)又出現(xiàn)兩個(gè)線程同時(shí)持有鎖了。

    總結(jié)

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

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

    国产疯狂伦交大片 | 国产成人亚洲综合无码 | 老子影院午夜伦不卡 | 亚洲另类伦春色综合小说 | 少妇愉情理伦片bd | 欧美一区二区三区视频在线观看 | 丰满护士巨好爽好大乳 | 无码国产激情在线观看 | 国精产品一区二区三区 | 亚洲中文字幕无码一久久区 | 国产后入清纯学生妹 | 色欲综合久久中文字幕网 | 国产精品亚洲综合色区韩国 | 国产一精品一av一免费 | 欧美性生交xxxxx久久久 | 精品人人妻人人澡人人爽人人 | 国产97人人超碰caoprom | 欧美性猛交xxxx富婆 | 免费无码一区二区三区蜜桃大 | 人妻尝试又大又粗久久 | 亚洲性无码av中文字幕 | 国产精品a成v人在线播放 | 久久精品中文闷骚内射 | 色五月五月丁香亚洲综合网 | 久久国产精品偷任你爽任你 | 成人精品天堂一区二区三区 | 日本va欧美va欧美va精品 | 日韩人妻无码一区二区三区久久99 | 久久精品成人欧美大片 | 免费播放一区二区三区 | 兔费看少妇性l交大片免费 | 久久久久久国产精品无码下载 | 噜噜噜亚洲色成人网站 | 久久精品人人做人人综合试看 | 色五月丁香五月综合五月 | 蜜臀av在线播放 久久综合激激的五月天 | 国产极品美女高潮无套在线观看 | 成人一在线视频日韩国产 | 丁香啪啪综合成人亚洲 | 亚洲精品美女久久久久久久 | 久久精品一区二区三区四区 | 色综合久久久久综合一本到桃花网 | 色婷婷综合中文久久一本 | 国产精品嫩草久久久久 | 国产做国产爱免费视频 | 亚洲熟妇色xxxxx欧美老妇y | 漂亮人妻洗澡被公强 日日躁 | 国产人妻精品一区二区三区不卡 | 精品偷自拍另类在线观看 | 无码人中文字幕 | 欧美zoozzooz性欧美 | 日本在线高清不卡免费播放 | 亚洲中文字幕av在天堂 | 亚洲熟女一区二区三区 | 欧美精品在线观看 | 久久综合色之久久综合 | 999久久久国产精品消防器材 | 国产成人久久精品流白浆 | 日本一卡2卡3卡四卡精品网站 | 久久99热只有频精品8 | 色五月五月丁香亚洲综合网 | 欧美日韩综合一区二区三区 | 曰韩少妇内射免费播放 | 爱做久久久久久 | 亚洲理论电影在线观看 | 六月丁香婷婷色狠狠久久 | 亚洲精品鲁一鲁一区二区三区 | 久久 国产 尿 小便 嘘嘘 | 麻豆果冻传媒2021精品传媒一区下载 | 亚洲爆乳精品无码一区二区三区 | 精品熟女少妇av免费观看 | 国产后入清纯学生妹 | 色综合久久久久综合一本到桃花网 | 午夜丰满少妇性开放视频 | 大地资源网第二页免费观看 | 精品国产乱码久久久久乱码 | 国产无av码在线观看 | 亚洲熟悉妇女xxx妇女av | 国产国语老龄妇女a片 | 少妇性l交大片 | 婷婷综合久久中文字幕蜜桃三电影 | 一区二区三区乱码在线 | 欧洲 | 色五月五月丁香亚洲综合网 | 亚洲精品一区二区三区婷婷月 | 国产精品内射视频免费 | 波多野结衣aⅴ在线 | 无码av最新清无码专区吞精 | 18无码粉嫩小泬无套在线观看 | 亚洲色欲色欲天天天www | 伊人久久大香线焦av综合影院 | 亚洲日韩一区二区 | 亚洲国产精品成人久久蜜臀 | 亚洲男人av香蕉爽爽爽爽 | 激情人妻另类人妻伦 | 性欧美牲交xxxxx视频 | 国产激情艳情在线看视频 | 国产做国产爱免费视频 | 中文久久乱码一区二区 | 图片区 小说区 区 亚洲五月 | 性生交片免费无码看人 | 九月婷婷人人澡人人添人人爽 | 东京无码熟妇人妻av在线网址 | 精品乱码久久久久久久 | 男人的天堂av网站 | 亚洲熟悉妇女xxx妇女av | 久久国产精品二国产精品 | 午夜肉伦伦影院 | 亚洲熟悉妇女xxx妇女av | 国产在线无码精品电影网 | 自拍偷自拍亚洲精品被多人伦好爽 | 欧美性黑人极品hd | 青春草在线视频免费观看 | 日日干夜夜干 | 综合网日日天干夜夜久久 | 日日天干夜夜狠狠爱 | 国产亚洲精品久久久久久久 | 无码国模国产在线观看 | 美女黄网站人色视频免费国产 | 人人妻人人澡人人爽人人精品 | 国产成人无码午夜视频在线观看 | 无码任你躁久久久久久久 | 一本久久a久久精品亚洲 | 国产精品无码一区二区三区不卡 | 亚洲小说图区综合在线 | 少妇无套内谢久久久久 | 装睡被陌生人摸出水好爽 | 亚洲aⅴ无码成人网站国产app | 国产尤物精品视频 | 青青青手机频在线观看 | 国内揄拍国内精品人妻 | 成人免费视频在线观看 | 国产精品视频免费播放 | 中文字幕人成乱码熟女app | 日韩av无码中文无码电影 | 两性色午夜视频免费播放 | 一本精品99久久精品77 | 午夜精品久久久内射近拍高清 | 中文久久乱码一区二区 | 老司机亚洲精品影院无码 | 激情五月综合色婷婷一区二区 | 亚洲国产精品毛片av不卡在线 | 亚洲欧洲日本无在线码 | 大胆欧美熟妇xx | 性史性农村dvd毛片 | 国产成人无码av片在线观看不卡 | 色诱久久久久综合网ywww | 最近免费中文字幕中文高清百度 | 国产办公室秘书无码精品99 | 少妇邻居内射在线 | 国产偷自视频区视频 | 大肉大捧一进一出好爽视频 | 国产黄在线观看免费观看不卡 | 欧美 日韩 人妻 高清 中文 | www一区二区www免费 | 国产午夜无码精品免费看 | 国产一区二区不卡老阿姨 | 在线精品亚洲一区二区 | 亚洲中文无码av永久不收费 | 女高中生第一次破苞av | 亚洲精品国产a久久久久久 | 狠狠亚洲超碰狼人久久 | 国产精品久久久久无码av色戒 | 日本va欧美va欧美va精品 | 久久精品视频在线看15 | 午夜性刺激在线视频免费 | 水蜜桃av无码 | 男女爱爱好爽视频免费看 | 欧美日本精品一区二区三区 | 亚洲熟妇色xxxxx亚洲 | 性欧美牲交xxxxx视频 | 正在播放老肥熟妇露脸 | 亚洲成a人一区二区三区 | 东京热一精品无码av | 久久精品国产大片免费观看 | 国产成人一区二区三区在线观看 | 国产精品美女久久久网av | 婷婷五月综合缴情在线视频 | 天天燥日日燥 | 日韩无套无码精品 | 水蜜桃av无码 | 国产色在线 | 国产 | 人妻中文无码久热丝袜 | 日韩人妻少妇一区二区三区 | 4hu四虎永久在线观看 | 国产乱子伦视频在线播放 | 一区二区传媒有限公司 | 一个人看的www免费视频在线观看 | 人人澡人人透人人爽 | 亚洲人成影院在线无码按摩店 | 亚洲精品一区二区三区大桥未久 | 精品人妻中文字幕有码在线 | 精品午夜福利在线观看 | 中文毛片无遮挡高清免费 | 国产精品对白交换视频 | 日本www一道久久久免费榴莲 | 久久久久久久久蜜桃 | 女人被男人爽到呻吟的视频 | 露脸叫床粗话东北少妇 | 亚洲大尺度无码无码专区 | 亚洲成av人在线观看网址 | 亚洲爆乳精品无码一区二区三区 | 中文字幕乱码中文乱码51精品 | 未满成年国产在线观看 | 亚洲一区二区观看播放 | 女人被男人爽到呻吟的视频 | 国产成人无码一二三区视频 | 熟妇人妻无乱码中文字幕 | 亚洲爆乳精品无码一区二区三区 | 欧美老人巨大xxxx做受 | 无码人妻av免费一区二区三区 | 国模大胆一区二区三区 | 无码成人精品区在线观看 | 国产激情无码一区二区app | 最近免费中文字幕中文高清百度 | 97久久国产亚洲精品超碰热 | 精品亚洲成av人在线观看 | 国产精品美女久久久 | 久久人人爽人人爽人人片ⅴ | 久久久久久久人妻无码中文字幕爆 | 亚洲啪av永久无码精品放毛片 | 久久这里只有精品视频9 | 性生交大片免费看l | 国产精品二区一区二区aⅴ污介绍 | 国产成人无码av一区二区 | 两性色午夜视频免费播放 | 天天av天天av天天透 | 亚洲 日韩 欧美 成人 在线观看 | 精品人妻av区 | 欧美日本免费一区二区三区 | 亚洲va欧美va天堂v国产综合 | 精品国偷自产在线视频 | 欧美性猛交内射兽交老熟妇 | 国产精品无码成人午夜电影 | 国产一区二区三区日韩精品 | 午夜免费福利小电影 | 小鲜肉自慰网站xnxx | 国产精品人人爽人人做我的可爱 | 人妻有码中文字幕在线 | 国产午夜无码视频在线观看 | 精品国产aⅴ无码一区二区 | 18禁黄网站男男禁片免费观看 | 亚洲无人区午夜福利码高清完整版 | 亚洲热妇无码av在线播放 | 夜夜高潮次次欢爽av女 | 国产精品自产拍在线观看 | 国产精品久久久久久亚洲影视内衣 | 国产精品va在线播放 | 日本一区二区三区免费高清 | 精品成人av一区二区三区 | 国产电影无码午夜在线播放 | 成人精品视频一区二区三区尤物 | 日本www一道久久久免费榴莲 | 人人妻人人澡人人爽欧美一区 | 樱花草在线社区www | 国模大胆一区二区三区 | 99精品视频在线观看免费 | 无码av免费一区二区三区试看 | 久久99精品国产.久久久久 | 俄罗斯老熟妇色xxxx | 兔费看少妇性l交大片免费 | 亚洲 欧美 激情 小说 另类 | 欧美日韩色另类综合 | 捆绑白丝粉色jk震动捧喷白浆 | 国产人妻大战黑人第1集 | 丰满少妇人妻久久久久久 | 亚洲人成网站在线播放942 | 国产精华av午夜在线观看 | 成熟女人特级毛片www免费 | 亚洲国产综合无码一区 | 国产亚洲人成a在线v网站 | 人人妻人人澡人人爽欧美精品 | 99久久精品国产一区二区蜜芽 | 四虎永久在线精品免费网址 | 日韩av激情在线观看 | 日韩 欧美 动漫 国产 制服 | 久久久久久九九精品久 | 中文字幕无码日韩欧毛 | 亚洲狠狠婷婷综合久久 | 欧美老人巨大xxxx做受 | 午夜无码区在线观看 | 内射后入在线观看一区 | 亚洲精品久久久久中文第一幕 | 日韩精品无码一本二本三本色 | 国产av剧情md精品麻豆 | 好男人www社区 | 国产熟女一区二区三区四区五区 | 东北女人啪啪对白 | 啦啦啦www在线观看免费视频 | 久久精品人妻少妇一区二区三区 | 国产人妻人伦精品1国产丝袜 | 无码人妻丰满熟妇区毛片18 | 国产sm调教视频在线观看 | 最近中文2019字幕第二页 | 国产真实乱对白精彩久久 | 熟妇女人妻丰满少妇中文字幕 | 高清无码午夜福利视频 | 国产人妻精品一区二区三区不卡 | 午夜福利试看120秒体验区 | 国产精品爱久久久久久久 | 成人综合网亚洲伊人 | 久久精品人人做人人综合 | 久久久精品456亚洲影院 | 亚洲精品一区二区三区大桥未久 | 亚洲国产欧美国产综合一区 | 丝袜足控一区二区三区 | 日韩成人一区二区三区在线观看 | 久久五月精品中文字幕 | 国产真实伦对白全集 | 牲欲强的熟妇农村老妇女视频 | 国语精品一区二区三区 | 曰韩无码二三区中文字幕 | 亚洲精品国产a久久久久久 | 九九热爱视频精品 | 国产午夜亚洲精品不卡 | 女人被男人爽到呻吟的视频 | 无遮挡国产高潮视频免费观看 | 亚洲 a v无 码免 费 成 人 a v | 日本护士毛茸茸高潮 | 精品无码一区二区三区的天堂 | 水蜜桃亚洲一二三四在线 | 十八禁视频网站在线观看 | 少妇高潮一区二区三区99 | 撕开奶罩揉吮奶头视频 | 国产成人无码专区 | 99久久精品国产一区二区蜜芽 | 久在线观看福利视频 | 丰满妇女强制高潮18xxxx | 欧美 亚洲 国产 另类 | 亚无码乱人伦一区二区 | 久9re热视频这里只有精品 | 国产午夜亚洲精品不卡下载 | 99精品无人区乱码1区2区3区 | 欧美一区二区三区视频在线观看 | 少妇的肉体aa片免费 | 欧美黑人性暴力猛交喷水 | 久久人人爽人人爽人人片av高清 | 日韩精品无码一区二区中文字幕 | 大肉大捧一进一出视频出来呀 | 久久精品国产一区二区三区 | 国语精品一区二区三区 | 久久天天躁夜夜躁狠狠 | 5858s亚洲色大成网站www | 蜜臀aⅴ国产精品久久久国产老师 | 成人精品天堂一区二区三区 | 亚洲中文字幕va福利 | 国产亚洲美女精品久久久2020 | av无码久久久久不卡免费网站 | 亚洲成色www久久网站 | 国产国语老龄妇女a片 | 丰满诱人的人妻3 | 国产精品手机免费 | 久久精品99久久香蕉国产色戒 | 欧美老妇交乱视频在线观看 | 久久精品人人做人人综合 | 中文字幕乱码人妻无码久久 | 久久久久av无码免费网 | 免费无码的av片在线观看 | 熟妇人妻无码xxx视频 | 毛片内射-百度 | 四虎永久在线精品免费网址 | 人妻无码αv中文字幕久久琪琪布 | 成人性做爰aaa片免费看 | 国产成人精品必看 | 国产精品视频免费播放 | а天堂中文在线官网 | 国产suv精品一区二区五 | 中文字幕色婷婷在线视频 | 国产乱码精品一品二品 | 天堂无码人妻精品一区二区三区 | 人人爽人人澡人人人妻 | 国产亚洲精品久久久闺蜜 | 色综合久久久无码中文字幕 | 国产精品久久久久久久9999 | 中文字幕色婷婷在线视频 | 嫩b人妻精品一区二区三区 | 国产乱子伦视频在线播放 | 精品国产aⅴ无码一区二区 | 精品水蜜桃久久久久久久 | 精品一区二区三区无码免费视频 | 欧美熟妇另类久久久久久不卡 | 一本大道久久东京热无码av | 亚洲热妇无码av在线播放 | 亚洲熟妇色xxxxx亚洲 | 免费播放一区二区三区 | 色五月丁香五月综合五月 | 精品国产一区av天美传媒 | 人妻与老人中文字幕 | 亚洲色在线无码国产精品不卡 | aⅴ亚洲 日韩 色 图网站 播放 | 成人性做爰aaa片免费看不忠 | 久久久久av无码免费网 | 国产高潮视频在线观看 | 野外少妇愉情中文字幕 | 亚洲 a v无 码免 费 成 人 a v | 亚洲色偷偷男人的天堂 | 18无码粉嫩小泬无套在线观看 | 男女猛烈xx00免费视频试看 | 久久久中文字幕日本无吗 | 亚洲综合色区中文字幕 | 国产97人人超碰caoprom | 欧美性生交xxxxx久久久 | 十八禁视频网站在线观看 | 日日麻批免费40分钟无码 | 中文字幕无码人妻少妇免费 | 青青久在线视频免费观看 | 四虎永久在线精品免费网址 | 97se亚洲精品一区 | 欧美一区二区三区视频在线观看 | 国产精品.xx视频.xxtv | 中文字幕日产无线码一区 | 欧美兽交xxxx×视频 | 亚洲男人av香蕉爽爽爽爽 | 国产人妖乱国产精品人妖 | 日本护士毛茸茸高潮 | 亚洲成av人片在线观看无码不卡 | 国产欧美亚洲精品a | 男人和女人高潮免费网站 | 在线精品亚洲一区二区 | 国产精品人人妻人人爽 | 人人妻人人澡人人爽人人精品浪潮 | 欧美自拍另类欧美综合图片区 | 无码播放一区二区三区 | 国产综合久久久久鬼色 | 九九在线中文字幕无码 | 好屌草这里只有精品 | 日本乱人伦片中文三区 | 国产精品二区一区二区aⅴ污介绍 | 欧美高清在线精品一区 | 亚无码乱人伦一区二区 | 国产一区二区三区四区五区加勒比 | av人摸人人人澡人人超碰下载 | 精品欧美一区二区三区久久久 | 九九热爱视频精品 | 一区二区传媒有限公司 | 强辱丰满人妻hd中文字幕 | 日本精品人妻无码免费大全 | 俺去俺来也在线www色官网 | 日韩无码专区 | 又大又黄又粗又爽的免费视频 | 伊人久久大香线焦av综合影院 | 成人免费视频在线观看 | 国产成人精品视频ⅴa片软件竹菊 | 性史性农村dvd毛片 | 初尝人妻少妇中文字幕 | 国产一区二区三区影院 | 日韩视频 中文字幕 视频一区 | 内射爽无广熟女亚洲 | 亚洲精品鲁一鲁一区二区三区 | 少妇性俱乐部纵欲狂欢电影 | 搡女人真爽免费视频大全 | 人人妻在人人 | 欧美人与禽zoz0性伦交 | 黑人玩弄人妻中文在线 | 亚洲精品午夜国产va久久成人 | 久久久久久九九精品久 | 久久久久久久久蜜桃 | 极品尤物被啪到呻吟喷水 | 在线播放无码字幕亚洲 | 免费人成网站视频在线观看 | 色爱情人网站 | 一区二区三区乱码在线 | 欧洲 | 99久久久无码国产aaa精品 | 亚洲自偷自拍另类第1页 | 亚洲国产精品一区二区第一页 | 中文字幕av无码一区二区三区电影 | 99久久99久久免费精品蜜桃 | 撕开奶罩揉吮奶头视频 | 夜夜夜高潮夜夜爽夜夜爰爰 | 少妇激情av一区二区 | 九一九色国产 | 国产人妻大战黑人第1集 | 欧美精品无码一区二区三区 | 亚洲综合久久一区二区 | 亚洲精品国产a久久久久久 | 国产综合久久久久鬼色 | 国产综合在线观看 | 亚洲成熟女人毛毛耸耸多 | 久久久亚洲欧洲日产国码αv | 国产精品美女久久久久av爽李琼 | а√天堂www在线天堂小说 | 日韩人妻无码中文字幕视频 | 午夜精品一区二区三区在线观看 | 国产成人无码a区在线观看视频app | 国产精品无码久久av | 欧洲熟妇色 欧美 | 沈阳熟女露脸对白视频 | 免费人成在线观看网站 | 无码一区二区三区在线观看 | 国产 精品 自在自线 | 欧美黑人性暴力猛交喷水 | 久久久亚洲欧洲日产国码αv | 国产亚洲日韩欧美另类第八页 | 成人无码精品1区2区3区免费看 | 人妻与老人中文字幕 | 色一情一乱一伦一区二区三欧美 | 国产av一区二区精品久久凹凸 | 国内精品人妻无码久久久影院蜜桃 | 国产激情无码一区二区app | 色五月五月丁香亚洲综合网 | 亚洲精品美女久久久久久久 | 东京无码熟妇人妻av在线网址 | 沈阳熟女露脸对白视频 | 婷婷丁香六月激情综合啪 | 人妻插b视频一区二区三区 | 国产香蕉97碰碰久久人人 | 色一情一乱一伦一区二区三欧美 | 思思久久99热只有频精品66 | 久久久久亚洲精品男人的天堂 | 日韩精品无码一本二本三本色 | 丁香花在线影院观看在线播放 | 好男人www社区 | 成人试看120秒体验区 | 日韩精品无码免费一区二区三区 | 国产av久久久久精东av | 一本久久a久久精品vr综合 | 亚洲日韩一区二区 | 综合人妻久久一区二区精品 | 亚洲精品一区二区三区在线观看 | 免费无码一区二区三区蜜桃大 | 人人妻在人人 | 亚洲欧美精品aaaaaa片 | 亚洲一区二区三区四区 | 狂野欧美性猛交免费视频 | 在线a亚洲视频播放在线观看 | 亚洲日本在线电影 | 久久久国产精品无码免费专区 | 亚洲 高清 成人 动漫 | 久久久精品欧美一区二区免费 | 亚洲a无码综合a国产av中文 | 丝袜 中出 制服 人妻 美腿 | 国产又爽又猛又粗的视频a片 | 亚洲精品一区二区三区婷婷月 | 色婷婷香蕉在线一区二区 | 国产午夜福利100集发布 | 精品国产一区av天美传媒 | 国产三级精品三级男人的天堂 | 日本肉体xxxx裸交 | 99久久精品日本一区二区免费 | 午夜福利试看120秒体验区 | 中文字幕av无码一区二区三区电影 | 亚洲爆乳精品无码一区二区三区 | 人人妻人人澡人人爽欧美一区 | 天天拍夜夜添久久精品大 | 老子影院午夜伦不卡 | 日产国产精品亚洲系列 | 国产又粗又硬又大爽黄老大爷视 | 久久久中文字幕日本无吗 | 国产av剧情md精品麻豆 | 国产成人无码午夜视频在线观看 | 亚洲码国产精品高潮在线 | 人妻aⅴ无码一区二区三区 | 无码人妻丰满熟妇区毛片18 | 国产偷抇久久精品a片69 | 牲欲强的熟妇农村老妇女 | 国产在热线精品视频 | 无遮挡国产高潮视频免费观看 | 日本一卡二卡不卡视频查询 | 伊人久久大香线焦av综合影院 | 欧美日韩在线亚洲综合国产人 | 日日天日日夜日日摸 | 国产亚洲人成a在线v网站 | 妺妺窝人体色www婷婷 | 曰韩少妇内射免费播放 | 国产精品久久久久9999小说 | 国产午夜无码视频在线观看 | 国产午夜福利亚洲第一 | 精品国产乱码久久久久乱码 | 国产绳艺sm调教室论坛 | 中文字幕人妻无码一区二区三区 | 亚洲精品成人av在线 | 丰满护士巨好爽好大乳 | 亚洲精品国产a久久久久久 | 成人无码视频免费播放 | 日韩无码专区 | yw尤物av无码国产在线观看 | 内射老妇bbwx0c0ck | 乱人伦人妻中文字幕无码久久网 | 亚洲熟妇色xxxxx欧美老妇y | 亚洲国产精品久久久天堂 | 免费观看激色视频网站 | 久久99精品久久久久久动态图 | 日韩精品a片一区二区三区妖精 | 四虎永久在线精品免费网址 | 天天摸天天碰天天添 | 樱花草在线播放免费中文 | 成人一在线视频日韩国产 | 成人性做爰aaa片免费看 | 999久久久国产精品消防器材 | 色 综合 欧美 亚洲 国产 | 国内精品人妻无码久久久影院 | 亚洲中文字幕在线观看 | 亚洲成色www久久网站 | 狂野欧美激情性xxxx | 四十如虎的丰满熟妇啪啪 | 日韩欧美成人免费观看 | 日韩精品无码免费一区二区三区 | 免费人成在线视频无码 | 久久久久免费看成人影片 | 亚洲一区二区三区偷拍女厕 | 99精品国产综合久久久久五月天 | 亚洲精品国产精品乱码不卡 | 国产精品无码久久av | 国产成人综合色在线观看网站 | 亚洲中文字幕无码中字 | 无码成人精品区在线观看 | 麻豆精品国产精华精华液好用吗 | 小sao货水好多真紧h无码视频 | 中文字幕无线码免费人妻 | 中文字幕av伊人av无码av | 精品国产一区二区三区av 性色 | 在教室伦流澡到高潮hnp视频 | 波多野结衣 黑人 | 最近免费中文字幕中文高清百度 | 亚洲国产精品一区二区第一页 | 久久aⅴ免费观看 | 成人无码精品1区2区3区免费看 | 色综合天天综合狠狠爱 | 任你躁国产自任一区二区三区 | 正在播放老肥熟妇露脸 | 国产精品久久久一区二区三区 | 成人无码视频在线观看网站 | 亚洲国产成人av在线观看 | 亚洲综合伊人久久大杳蕉 | 国产小呦泬泬99精品 | 一本久道高清无码视频 | 欧美精品无码一区二区三区 | 成人一区二区免费视频 | 色婷婷综合中文久久一本 | 天天做天天爱天天爽综合网 | 黑森林福利视频导航 | 美女扒开屁股让男人桶 | 精品欧洲av无码一区二区三区 | 欧美激情内射喷水高潮 | 小sao货水好多真紧h无码视频 | 国产精品无码成人午夜电影 | 在线亚洲高清揄拍自拍一品区 | 久久久久久国产精品无码下载 | 欧美精品在线观看 | 久久久久久久人妻无码中文字幕爆 | 国产精品理论片在线观看 | 丰满少妇女裸体bbw | 久久久久久久人妻无码中文字幕爆 | 久久aⅴ免费观看 | 少女韩国电视剧在线观看完整 | 日韩欧美中文字幕公布 | 在线精品亚洲一区二区 | 狠狠色丁香久久婷婷综合五月 | 青草视频在线播放 | 久久精品国产一区二区三区 | 任你躁在线精品免费 | 荫蒂被男人添的好舒服爽免费视频 | 国产av一区二区精品久久凹凸 | 亚洲 欧美 激情 小说 另类 | 人妻熟女一区 | 国产精品无码一区二区三区不卡 | 精品一二三区久久aaa片 | 日韩人妻无码一区二区三区久久99 | 一本久久伊人热热精品中文字幕 | 99久久久无码国产精品免费 | 国产精品二区一区二区aⅴ污介绍 | 全黄性性激高免费视频 | 亚洲人成网站色7799 | 国产成人精品视频ⅴa片软件竹菊 | 午夜时刻免费入口 | 欧美熟妇另类久久久久久不卡 | 少妇高潮一区二区三区99 | 色婷婷久久一区二区三区麻豆 | 好屌草这里只有精品 | 成人影院yy111111在线观看 | 内射巨臀欧美在线视频 | 日日天日日夜日日摸 | 中文字幕人妻无码一区二区三区 | 中文字幕av无码一区二区三区电影 | 激情内射日本一区二区三区 | 鲁大师影院在线观看 | 国内丰满熟女出轨videos | 欧美丰满老熟妇xxxxx性 | av无码不卡在线观看免费 | 国产在线精品一区二区高清不卡 | 国产精品永久免费视频 | 少妇人妻av毛片在线看 | 久久天天躁夜夜躁狠狠 | 日日夜夜撸啊撸 | 日韩欧美中文字幕在线三区 | 大乳丰满人妻中文字幕日本 | 中文字幕人妻无码一区二区三区 | 未满成年国产在线观看 | 秋霞成人午夜鲁丝一区二区三区 | 亚洲欧美日韩成人高清在线一区 | 成年美女黄网站色大免费视频 | 亚洲成a人片在线观看日本 | 熟妇人妻无码xxx视频 | 无码福利日韩神码福利片 | 国产办公室秘书无码精品99 | 日本丰满护士爆乳xxxx | 欧美35页视频在线观看 | 亚洲精品国偷拍自产在线观看蜜桃 | 国产精品永久免费视频 | 伊人久久大香线焦av综合影院 | 97精品国产97久久久久久免费 | 国产乱人伦偷精品视频 | 麻豆果冻传媒2021精品传媒一区下载 | 国产亚洲精品久久久久久大师 | 日本熟妇人妻xxxxx人hd | 精品一区二区三区无码免费视频 | 熟女俱乐部五十路六十路av | 久久久久久久女国产乱让韩 | 97精品人妻一区二区三区香蕉 | 亚洲欧洲中文日韩av乱码 | 伊人久久大香线蕉午夜 | 全黄性性激高免费视频 | 少妇的肉体aa片免费 | 国语自产偷拍精品视频偷 | 丰满岳乱妇在线观看中字无码 | 午夜成人1000部免费视频 | 欧美精品一区二区精品久久 | 无遮挡啪啪摇乳动态图 | 人妻与老人中文字幕 | 国产精品国产自线拍免费软件 | 高清国产亚洲精品自在久久 | 久久精品国产一区二区三区 | 国产欧美亚洲精品a | 久9re热视频这里只有精品 | 亚洲人亚洲人成电影网站色 | 午夜精品久久久内射近拍高清 | 午夜成人1000部免费视频 | 激情五月综合色婷婷一区二区 | 无码任你躁久久久久久久 | 国产精品美女久久久网av | 狠狠综合久久久久综合网 | 国产无av码在线观看 | 无码人妻精品一区二区三区不卡 | 精品人妻中文字幕有码在线 | 成 人 网 站国产免费观看 | 国产凸凹视频一区二区 | 大肉大捧一进一出视频出来呀 | 日韩av无码一区二区三区不卡 | 国产猛烈高潮尖叫视频免费 | 国产免费久久精品国产传媒 | 亚洲国产欧美日韩精品一区二区三区 | 色妞www精品免费视频 | 四虎4hu永久免费 | 亚洲日韩乱码中文无码蜜桃臀网站 | 18黄暴禁片在线观看 | 熟女少妇在线视频播放 | 久久精品中文闷骚内射 | 欧美老妇交乱视频在线观看 | 国产偷国产偷精品高清尤物 | 97精品国产97久久久久久免费 | 国产精品多人p群无码 | 精品成在人线av无码免费看 | 人妻互换免费中文字幕 | 久久综合九色综合97网 | www国产精品内射老师 | 日韩亚洲欧美中文高清在线 | 国产av无码专区亚洲a∨毛片 | 国产美女精品一区二区三区 | 欧美亚洲日韩国产人成在线播放 | 久久亚洲精品中文字幕无男同 | 亚洲熟妇色xxxxx欧美老妇 | 131美女爱做视频 | 无码国产乱人伦偷精品视频 | 乱人伦中文视频在线观看 | 精品日本一区二区三区在线观看 | 国产午夜亚洲精品不卡下载 | 久久人人97超碰a片精品 | 国产9 9在线 | 中文 | 国产人妻精品一区二区三区 | 国产成人无码av在线影院 | 永久免费观看国产裸体美女 | 欧美怡红院免费全部视频 | 中文字幕 亚洲精品 第1页 | 久久久久久av无码免费看大片 | 日韩人妻无码中文字幕视频 | 中文字幕乱码人妻无码久久 | 大屁股大乳丰满人妻 | 日韩在线不卡免费视频一区 | 在线观看国产一区二区三区 | 偷窥日本少妇撒尿chinese | 内射后入在线观看一区 | 国产超碰人人爽人人做人人添 | 久久久久人妻一区精品色欧美 | 图片区 小说区 区 亚洲五月 | 国产精品亚洲专区无码不卡 | 丝袜 中出 制服 人妻 美腿 | av在线亚洲欧洲日产一区二区 | 一本久道高清无码视频 | 国产精品美女久久久久av爽李琼 | 三级4级全黄60分钟 | 久久久中文久久久无码 | 无码人妻少妇伦在线电影 | 内射欧美老妇wbb | 偷窥日本少妇撒尿chinese | 久久久久久国产精品无码下载 | 国产精品久久精品三级 | 成人影院yy111111在线观看 | 麻豆av传媒蜜桃天美传媒 | 黑森林福利视频导航 | 婷婷色婷婷开心五月四房播播 | 婷婷综合久久中文字幕蜜桃三电影 | 国产情侣作爱视频免费观看 | 真人与拘做受免费视频一 | 国产亚洲人成在线播放 | 国产一精品一av一免费 | 丰满护士巨好爽好大乳 | 国产精品高潮呻吟av久久4虎 | 欧美阿v高清资源不卡在线播放 | 欧美自拍另类欧美综合图片区 | 无码成人精品区在线观看 | 欧美猛少妇色xxxxx | 无码福利日韩神码福利片 | 性生交片免费无码看人 | 人妻无码久久精品人妻 | 国产人妻精品一区二区三区不卡 | 欧美 日韩 亚洲 在线 | 欧美兽交xxxx×视频 | 国产精品亚洲专区无码不卡 | 天天做天天爱天天爽综合网 | 又紧又大又爽精品一区二区 | 成人aaa片一区国产精品 | 人人超人人超碰超国产 | 婷婷五月综合缴情在线视频 | 精品国产成人一区二区三区 | 中文字幕av伊人av无码av | а√资源新版在线天堂 | 色婷婷久久一区二区三区麻豆 | 麻豆国产人妻欲求不满谁演的 | 亚洲精品一区二区三区在线 | 国产成人一区二区三区在线观看 | 亚洲人亚洲人成电影网站色 | 精品午夜福利在线观看 | 久久亚洲中文字幕精品一区 | 亚洲人成人无码网www国产 | 任你躁国产自任一区二区三区 | 2020久久超碰国产精品最新 | 国产香蕉尹人视频在线 | 日本精品少妇一区二区三区 | 久久久国产精品无码免费专区 | 荡女精品导航 | 国产九九九九九九九a片 | 色婷婷久久一区二区三区麻豆 | 精品人妻人人做人人爽夜夜爽 | 7777奇米四色成人眼影 | 成人无码精品1区2区3区免费看 | 亚洲精品久久久久avwww潮水 | 狠狠色噜噜狠狠狠7777奇米 | 亚洲精品综合五月久久小说 | 国产亚洲精品久久久久久 | 国产成人精品视频ⅴa片软件竹菊 | 小sao货水好多真紧h无码视频 | 亚洲国产av精品一区二区蜜芽 | 亚洲 高清 成人 动漫 | 亚洲毛片av日韩av无码 | 日韩视频 中文字幕 视频一区 | 国产人妖乱国产精品人妖 | 亚洲国产高清在线观看视频 | 午夜福利不卡在线视频 | 少妇久久久久久人妻无码 | 久久久久99精品成人片 | 久久精品国产大片免费观看 | 日本www一道久久久免费榴莲 | 中文字幕久久久久人妻 | 国产真实乱对白精彩久久 | 无码av岛国片在线播放 | 国产在线精品一区二区高清不卡 | 乱人伦人妻中文字幕无码久久网 | 一本色道久久综合狠狠躁 | 久久人人爽人人人人片 | 国产猛烈高潮尖叫视频免费 | 精品一区二区不卡无码av | 欧美成人免费全部网站 | 少女韩国电视剧在线观看完整 | 亚洲高清偷拍一区二区三区 | 国产精品va在线观看无码 | 日本一本二本三区免费 | 亚洲自偷自偷在线制服 | 久久久久久a亚洲欧洲av冫 | 久久天天躁夜夜躁狠狠 | 性欧美疯狂xxxxbbbb | 欧美人与动性行为视频 | 亚洲精品成人福利网站 | 牛和人交xxxx欧美 | 少妇性俱乐部纵欲狂欢电影 | 欧美亚洲日韩国产人成在线播放 | 波多野结衣av一区二区全免费观看 | 国产精品美女久久久网av | 丝袜美腿亚洲一区二区 | 欧美自拍另类欧美综合图片区 | 一区二区传媒有限公司 | 76少妇精品导航 | 成 人 免费观看网站 | 狠狠综合久久久久综合网 | 99久久精品日本一区二区免费 | 欧美日韩一区二区三区自拍 | 成人无码影片精品久久久 | 精品欧美一区二区三区久久久 | www国产亚洲精品久久久日本 | 国产成人无码区免费内射一片色欲 | 国产片av国语在线观看 | 亚洲欧美精品伊人久久 | 鲁鲁鲁爽爽爽在线视频观看 | 亚洲精品久久久久avwww潮水 | 亚洲欧洲中文日韩av乱码 | 少妇人妻av毛片在线看 | 国产精品无码一区二区三区不卡 | 国产精品无码mv在线观看 | 97久久超碰中文字幕 | 亚洲熟妇色xxxxx欧美老妇 | 亚洲第一无码av无码专区 | 久久久久久久人妻无码中文字幕爆 | 久久伊人色av天堂九九小黄鸭 | 精品国产一区二区三区四区 | 国产亚洲人成a在线v网站 | 18禁黄网站男男禁片免费观看 | 99精品国产综合久久久久五月天 | 国产免费无码一区二区视频 | 国产精华av午夜在线观看 | 伊人久久婷婷五月综合97色 | 国产成人综合在线女婷五月99播放 | 国内精品九九久久久精品 | 国产三级久久久精品麻豆三级 | 狂野欧美性猛交免费视频 | www国产亚洲精品久久网站 | 亚拍精品一区二区三区探花 | 亚洲男人av香蕉爽爽爽爽 | 亚洲另类伦春色综合小说 | 永久黄网站色视频免费直播 | 俄罗斯老熟妇色xxxx | 美女黄网站人色视频免费国产 | 无码午夜成人1000部免费视频 | a国产一区二区免费入口 | 亚洲中文字幕av在天堂 | 欧美国产亚洲日韩在线二区 | 精品国产av色一区二区深夜久久 | 欧美精品免费观看二区 | 丰满人妻一区二区三区免费视频 | 久久精品视频在线看15 | 99久久久无码国产aaa精品 | 久久国内精品自在自线 | 99久久精品日本一区二区免费 | 荡女精品导航 | 沈阳熟女露脸对白视频 | 白嫩日本少妇做爰 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 国产香蕉97碰碰久久人人 | 国产精品亚洲五月天高清 | 最新国产麻豆aⅴ精品无码 | 日本va欧美va欧美va精品 | 99精品国产综合久久久久五月天 | 欧美人与动性行为视频 | 欧美日韩一区二区免费视频 | 亚洲一区二区三区无码久久 | 久久久久久九九精品久 | 亚洲经典千人经典日产 | 免费无码一区二区三区蜜桃大 | 久久国产精品二国产精品 | 丰腴饱满的极品熟妇 | 亚洲男女内射在线播放 | 中文字幕日韩精品一区二区三区 | 国产三级精品三级男人的天堂 | 在线欧美精品一区二区三区 | 伊人久久大香线焦av综合影院 | 人人妻人人澡人人爽精品欧美 | 青草视频在线播放 | 精品亚洲韩国一区二区三区 | 精品人人妻人人澡人人爽人人 | 人妻无码αv中文字幕久久琪琪布 | 最近中文2019字幕第二页 | 麻豆国产97在线 | 欧洲 | www国产亚洲精品久久网站 | 成人精品天堂一区二区三区 | 欧美兽交xxxx×视频 | 欧美肥老太牲交大战 | 中文久久乱码一区二区 | 亚洲精品一区二区三区婷婷月 | 四虎永久在线精品免费网址 | 国产精品18久久久久久麻辣 | 色老头在线一区二区三区 | 久久久久久九九精品久 | 日韩精品无码免费一区二区三区 | 亚洲国产精品成人久久蜜臀 | 国产人妻精品一区二区三区 | 粗大的内捧猛烈进出视频 | 久久亚洲中文字幕无码 | 国产99久久精品一区二区 | 女人被男人躁得好爽免费视频 | 国产精品多人p群无码 | 99久久亚洲精品无码毛片 | 国产人妻精品一区二区三区 | 秋霞成人午夜鲁丝一区二区三区 | 在线播放免费人成毛片乱码 | 俺去俺来也www色官网 | 黑人玩弄人妻中文在线 | 内射巨臀欧美在线视频 | 啦啦啦www在线观看免费视频 | 无遮挡国产高潮视频免费观看 | 亚洲中文字幕成人无码 | 97夜夜澡人人爽人人喊中国片 | 国产在线一区二区三区四区五区 | 国产精品久久久久久久9999 | 巨爆乳无码视频在线观看 | 熟妇人妻激情偷爽文 | 伊在人天堂亚洲香蕉精品区 | 人人澡人人妻人人爽人人蜜桃 | 亚洲日韩中文字幕在线播放 | 黑森林福利视频导航 | 亚洲人交乣女bbw | v一区无码内射国产 | 中文字幕无线码免费人妻 | yw尤物av无码国产在线观看 | 亚洲国产av精品一区二区蜜芽 | av香港经典三级级 在线 | 国产午夜亚洲精品不卡 | 夜夜夜高潮夜夜爽夜夜爰爰 | 初尝人妻少妇中文字幕 | 少妇被黑人到高潮喷出白浆 | 欧美亚洲日韩国产人成在线播放 | 精品熟女少妇av免费观看 | 欧美熟妇另类久久久久久多毛 | 最近免费中文字幕中文高清百度 | 欧美人与动性行为视频 | 麻豆蜜桃av蜜臀av色欲av | 亚洲精品综合五月久久小说 | 在线а√天堂中文官网 | 亚洲热妇无码av在线播放 | 天堂无码人妻精品一区二区三区 | 国产人妖乱国产精品人妖 | 国产精品无码久久av | 久久精品中文字幕一区 | 九九综合va免费看 | 性色欲情网站iwww九文堂 | 欧美大屁股xxxxhd黑色 | 欧美熟妇另类久久久久久不卡 | 初尝人妻少妇中文字幕 | 曰本女人与公拘交酡免费视频 | 青青久在线视频免费观看 | 久热国产vs视频在线观看 | 国产色视频一区二区三区 | 亚洲区欧美区综合区自拍区 | 日本熟妇人妻xxxxx人hd | 奇米影视7777久久精品 | 成人免费无码大片a毛片 | 欧美人与善在线com | 99国产欧美久久久精品 | 国产欧美亚洲精品a | 国产精品理论片在线观看 | 日产精品99久久久久久 | 成人欧美一区二区三区 | 午夜精品久久久久久久久 | 高潮毛片无遮挡高清免费 | 无码av中文字幕免费放 | 日韩亚洲欧美中文高清在线 | 熟女少妇人妻中文字幕 | 午夜性刺激在线视频免费 | 日本一本二本三区免费 | 成人无码视频在线观看网站 | 国产激情艳情在线看视频 | 亚洲综合精品香蕉久久网 | 亚洲精品国偷拍自产在线观看蜜桃 | 强伦人妻一区二区三区视频18 | 丝袜 中出 制服 人妻 美腿 | 国产精品无码成人午夜电影 | 麻豆国产丝袜白领秘书在线观看 | 精品久久8x国产免费观看 | 亚洲中文字幕久久无码 | 久久久久久九九精品久 | 精品久久久中文字幕人妻 | 欧美日韩亚洲国产精品 | 日韩人妻少妇一区二区三区 | 少妇被粗大的猛进出69影院 | 欧美人与禽猛交狂配 | 无码av岛国片在线播放 | 欧美成人家庭影院 | 成人无码视频在线观看网站 | 久久精品中文字幕一区 | 久久久国产一区二区三区 | 国产在线精品一区二区三区直播 | 东京热一精品无码av | 久久www免费人成人片 | 久久亚洲日韩精品一区二区三区 | 色五月五月丁香亚洲综合网 | 免费国产黄网站在线观看 | 日本一卡二卡不卡视频查询 | 人妻夜夜爽天天爽三区 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 久久综合久久自在自线精品自 | 色婷婷综合中文久久一本 | 色婷婷欧美在线播放内射 | 蜜桃无码一区二区三区 | 日韩av无码一区二区三区不卡 | 亚洲熟妇色xxxxx亚洲 | 东京无码熟妇人妻av在线网址 | 久久99精品久久久久久动态图 | 国产在线精品一区二区高清不卡 | 国产深夜福利视频在线 | 色婷婷综合激情综在线播放 | 国产精品亚洲а∨无码播放麻豆 | 麻豆果冻传媒2021精品传媒一区下载 | 激情内射日本一区二区三区 | 国产肉丝袜在线观看 | 97色伦图片97综合影院 | 欧美猛少妇色xxxxx | 国内少妇偷人精品视频免费 | 久久亚洲日韩精品一区二区三区 | 久久人妻内射无码一区三区 | 日产国产精品亚洲系列 | 国内精品九九久久久精品 | 波多野结衣av一区二区全免费观看 | 丰满少妇熟乱xxxxx视频 | 成人欧美一区二区三区黑人 | 成人精品视频一区二区 | 少妇高潮一区二区三区99 | 国产乱人伦av在线无码 | 日本精品人妻无码77777 天堂一区人妻无码 | 欧美亚洲日韩国产人成在线播放 | 亚洲中文字幕在线观看 | 欧美老妇与禽交 | 在线亚洲高清揄拍自拍一品区 | 亚洲精品一区三区三区在线观看 | 日本一本二本三区免费 | 中文字幕乱妇无码av在线 | 亚洲啪av永久无码精品放毛片 | 中文字幕无码免费久久99 | 澳门永久av免费网站 | 黑人大群体交免费视频 | 少妇无套内谢久久久久 | 亚洲精品美女久久久久久久 | 亚洲精品欧美二区三区中文字幕 | 久在线观看福利视频 | 久久zyz资源站无码中文动漫 | 国产av无码专区亚洲a∨毛片 | 亚洲精品成a人在线观看 | 亚拍精品一区二区三区探花 | 成人无码视频免费播放 | 3d动漫精品啪啪一区二区中 | 思思久久99热只有频精品66 | 无码人妻丰满熟妇区毛片18 | 国产精品香蕉在线观看 | 亚洲熟妇色xxxxx亚洲 | 综合激情五月综合激情五月激情1 | 一个人看的www免费视频在线观看 | 国产精品久久久久久久9999 | 精品无码一区二区三区的天堂 | 久久久国产一区二区三区 | 亚洲国产精品成人久久蜜臀 | 亚洲va欧美va天堂v国产综合 | 国内精品人妻无码久久久影院 | 国产精品香蕉在线观看 | 妺妺窝人体色www婷婷 | 中文字幕无码热在线视频 | 欧美黑人性暴力猛交喷水 | 无码人妻av免费一区二区三区 | 女人色极品影院 | 性史性农村dvd毛片 | 性欧美牲交在线视频 | 久久久久人妻一区精品色欧美 | 亚洲欧洲无卡二区视頻 | 国产精品美女久久久久av爽李琼 | 精品aⅴ一区二区三区 | 亚洲日本va中文字幕 | 丰满少妇熟乱xxxxx视频 | 性欧美大战久久久久久久 | 国产精品毛多多水多 | 丰满少妇弄高潮了www | 秋霞成人午夜鲁丝一区二区三区 | 99久久精品无码一区二区毛片 | 国产欧美熟妇另类久久久 | 精品人妻中文字幕有码在线 | 无遮挡啪啪摇乳动态图 | 三级4级全黄60分钟 | 内射白嫩少妇超碰 | 扒开双腿吃奶呻吟做受视频 | 丝袜人妻一区二区三区 | 国产色精品久久人妻 | 纯爱无遮挡h肉动漫在线播放 | 人妻与老人中文字幕 | 亚拍精品一区二区三区探花 | 国产av一区二区三区最新精品 | 76少妇精品导航 | 精品厕所偷拍各类美女tp嘘嘘 | 亚洲精品一区三区三区在线观看 | 波多野结衣av在线观看 | 国产亚洲精品久久久久久大师 | 日本精品人妻无码77777 天堂一区人妻无码 | 熟妇人妻无码xxx视频 | 国产偷国产偷精品高清尤物 | 亚洲日本va午夜在线电影 | 亚洲一区二区三区偷拍女厕 | 捆绑白丝粉色jk震动捧喷白浆 | 天堂在线观看www | 国产色精品久久人妻 | 欧美人与善在线com | 精品久久久久久人妻无码中文字幕 | 国产艳妇av在线观看果冻传媒 | 中文字幕乱妇无码av在线 | 西西人体www44rt大胆高清 | 无码人妻av免费一区二区三区 | 在线观看国产一区二区三区 | 日日天日日夜日日摸 | 国产乡下妇女做爰 | 国产亚洲精品久久久久久 | a在线亚洲男人的天堂 | 国产成人午夜福利在线播放 | 乱人伦人妻中文字幕无码久久网 | 中文字幕乱妇无码av在线 | 久久99精品国产麻豆蜜芽 | 国产亚洲精品久久久久久大师 | 国产婷婷色一区二区三区在线 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 装睡被陌生人摸出水好爽 | 97久久国产亚洲精品超碰热 | 伊在人天堂亚洲香蕉精品区 | 欧美丰满熟妇xxxx性ppx人交 | 精品久久久久久人妻无码中文字幕 | 夜夜影院未满十八勿进 | 欧美性猛交内射兽交老熟妇 | 亚洲精品一区二区三区大桥未久 | 精品夜夜澡人妻无码av蜜桃 | 5858s亚洲色大成网站www | 亚洲小说春色综合另类 | 成人无码精品1区2区3区免费看 | 中文字幕乱码亚洲无线三区 | 97精品国产97久久久久久免费 | 亚洲男女内射在线播放 | 日本一本二本三区免费 | 欧美怡红院免费全部视频 | 日韩人妻无码一区二区三区久久99 | 久久天天躁狠狠躁夜夜免费观看 | 亚洲精品一区国产 | 成年美女黄网站色大免费视频 | 露脸叫床粗话东北少妇 | 六十路熟妇乱子伦 | 亚洲综合精品香蕉久久网 | 性色av无码免费一区二区三区 | 免费看男女做好爽好硬视频 | 妺妺窝人体色www在线小说 | 欧美精品一区二区精品久久 | 久久久国产一区二区三区 | 天天躁夜夜躁狠狠是什么心态 | 久久久婷婷五月亚洲97号色 | 无码午夜成人1000部免费视频 | 久久久亚洲欧洲日产国码αv | 久久国产36精品色熟妇 | 免费网站看v片在线18禁无码 | 亚洲综合伊人久久大杳蕉 | 无码国内精品人妻少妇 | 国产美女精品一区二区三区 | 亚洲中文字幕在线无码一区二区 | 国内精品久久久久久中文字幕 | 国产超碰人人爽人人做人人添 | 天天拍夜夜添久久精品 | 欧洲vodafone精品性 | 成人一区二区免费视频 | 中文字幕无线码 | 亚洲 另类 在线 欧美 制服 | 亚洲中文字幕无码一久久区 | 在线观看国产一区二区三区 | 玩弄中年熟妇正在播放 | 久久天天躁狠狠躁夜夜免费观看 | 亚洲中文字幕乱码av波多ji | 久久人人爽人人爽人人片ⅴ | 久久国产自偷自偷免费一区调 | 久久国产精品二国产精品 | 国产精品人人爽人人做我的可爱 | 亚洲欧洲中文日韩av乱码 | 亚洲国产精品久久人人爱 | 国产精品久久久久久无码 | 亚洲人成人无码网www国产 | 青青青手机频在线观看 | 天堂在线观看www | 天天躁夜夜躁狠狠是什么心态 | 欧美丰满熟妇xxxx性ppx人交 | 激情五月综合色婷婷一区二区 | 亚洲国产精品一区二区美利坚 | 丰满人妻翻云覆雨呻吟视频 | 欧美日韩亚洲国产精品 | 日本精品少妇一区二区三区 | 中文精品久久久久人妻不卡 | 国产亚洲欧美日韩亚洲中文色 | 人人爽人人澡人人高潮 | 日韩精品a片一区二区三区妖精 | 国产色精品久久人妻 | 国产日产欧产精品精品app | 久久精品国产亚洲精品 | 无码人妻出轨黑人中文字幕 | 97精品人妻一区二区三区香蕉 | 国产真人无遮挡作爱免费视频 | 色老头在线一区二区三区 | 色综合久久88色综合天天 | 一二三四社区在线中文视频 | 97久久精品无码一区二区 | 国产又爽又猛又粗的视频a片 | 精品成人av一区二区三区 | 国产真实夫妇视频 | 无套内谢的新婚少妇国语播放 | 成人免费视频一区二区 | 国产精品办公室沙发 | 日韩人妻系列无码专区 | 欧美午夜特黄aaaaaa片 | 激情爆乳一区二区三区 | 久久久精品成人免费观看 | 国产亚洲人成a在线v网站 | 一二三四社区在线中文视频 | 精品偷拍一区二区三区在线看 | 国产香蕉尹人综合在线观看 | 国产在线一区二区三区四区五区 | 亚洲色偷偷偷综合网 | 久久久婷婷五月亚洲97号色 | 亚洲欧美色中文字幕在线 | 特黄特色大片免费播放器图片 | 女人高潮内射99精品 | 成年美女黄网站色大免费全看 | 精品乱子伦一区二区三区 | 国产午夜福利亚洲第一 | 亚洲午夜福利在线观看 | 亚洲理论电影在线观看 | 久久亚洲中文字幕精品一区 | 日本一本二本三区免费 | 无码乱肉视频免费大全合集 | 天堂а√在线地址中文在线 | 无码吃奶揉捏奶头高潮视频 | 黄网在线观看免费网站 | 久久精品人妻少妇一区二区三区 | 亚洲精品综合一区二区三区在线 | 天天做天天爱天天爽综合网 | 99在线 | 亚洲 | 波多野结衣高清一区二区三区 | 国产热a欧美热a在线视频 | 亚洲欧美日韩国产精品一区二区 | 亚洲欧美日韩综合久久久 | 国产极品美女高潮无套在线观看 | 少妇厨房愉情理9仑片视频 | 亚洲国产日韩a在线播放 | 中文字幕色婷婷在线视频 | 自拍偷自拍亚洲精品被多人伦好爽 | 欧美黑人巨大xxxxx | 欧美性生交活xxxxxdddd | 亚洲日本va中文字幕 | 丰满诱人的人妻3 | 麻豆md0077饥渴少妇 | 东京一本一道一二三区 | 国产办公室秘书无码精品99 | 人妻天天爽夜夜爽一区二区 | 日韩成人一区二区三区在线观看 | 欧美人与物videos另类 | 国产激情综合五月久久 | 国产亚洲精品久久久ai换 | 日韩无码专区 | 思思久久99热只有频精品66 | 无码中文字幕色专区 | 色 综合 欧美 亚洲 国产 | 亚洲 a v无 码免 费 成 人 a v | 2019nv天堂香蕉在线观看 | 久久99精品久久久久久动态图 | 色五月五月丁香亚洲综合网 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 国产精品久久精品三级 | yw尤物av无码国产在线观看 | 欧美日韩亚洲国产精品 | 中文字幕无码免费久久9一区9 | 国产午夜福利100集发布 | 亚洲综合无码一区二区三区 | 亚洲日韩乱码中文无码蜜桃臀网站 | 亚洲一区二区三区国产精华液 | 久久久久久久人妻无码中文字幕爆 | 亚洲 日韩 欧美 成人 在线观看 | 亚洲欧美国产精品专区久久 | 久久97精品久久久久久久不卡 | 丝袜人妻一区二区三区 | 国产三级精品三级男人的天堂 | 国产极品美女高潮无套在线观看 | 中文精品久久久久人妻不卡 | 亚洲一区二区三区偷拍女厕 | 最近免费中文字幕中文高清百度 | 亚洲国产日韩a在线播放 | 131美女爱做视频 | 欧美日韩亚洲国产精品 | 动漫av一区二区在线观看 | 久在线观看福利视频 | 欧洲vodafone精品性 | 麻豆人妻少妇精品无码专区 | 亚洲欧美中文字幕5发布 | 久久精品一区二区三区四区 | 亚洲国产精品成人久久蜜臀 | 日韩精品无码一本二本三本色 | 一本无码人妻在中文字幕免费 | 国产69精品久久久久app下载 | 欧美丰满少妇xxxx性 | 少妇无码av无码专区在线观看 | 蜜桃视频插满18在线观看 | 人人妻人人澡人人爽欧美一区 | 人妻插b视频一区二区三区 | 久久人妻内射无码一区三区 | 日韩人妻系列无码专区 | 久久久久久九九精品久 | 天天拍夜夜添久久精品大 | 欧美日韩久久久精品a片 | 大肉大捧一进一出好爽视频 | 日产精品99久久久久久 | 大地资源网第二页免费观看 | 国产女主播喷水视频在线观看 | 免费无码一区二区三区蜜桃大 | 少妇高潮一区二区三区99 | 中文字幕无码人妻少妇免费 | 免费无码一区二区三区蜜桃大 | 久久精品中文字幕一区 | aⅴ亚洲 日韩 色 图网站 播放 | 丰满人妻精品国产99aⅴ | 无码福利日韩神码福利片 | 午夜性刺激在线视频免费 | 男女超爽视频免费播放 | 疯狂三人交性欧美 | 国产精品二区一区二区aⅴ污介绍 | 久久综合给久久狠狠97色 | 中文字幕精品av一区二区五区 | 成在人线av无码免观看麻豆 | 国产精品无码成人午夜电影 | 国产绳艺sm调教室论坛 | 成人欧美一区二区三区黑人免费 | 中文字幕精品av一区二区五区 | 天干天干啦夜天干天2017 | 中文字幕 亚洲精品 第1页 | 国产午夜视频在线观看 | 国产人妻人伦精品 | 波多野42部无码喷潮在线 | 欧美成人高清在线播放 | 国产成人精品视频ⅴa片软件竹菊 | 中文字幕无码av波多野吉衣 | 亚洲国产精品一区二区第一页 | 未满小14洗澡无码视频网站 | 婷婷色婷婷开心五月四房播播 | 亚洲精品一区国产 | 国产sm调教视频在线观看 | 鲁一鲁av2019在线 | 无码人妻丰满熟妇区五十路百度 | 99久久久国产精品无码免费 | 国产精品亚洲一区二区三区喷水 | 一个人看的www免费视频在线观看 | 性开放的女人aaa片 | 波多野结衣av一区二区全免费观看 | 久久国内精品自在自线 | 国产又爽又黄又刺激的视频 | 一本久久a久久精品亚洲 | 牲欲强的熟妇农村老妇女 | 亚洲国产精品美女久久久久 | 国产成人一区二区三区别 | 无码精品国产va在线观看dvd | 婷婷五月综合缴情在线视频 | 午夜丰满少妇性开放视频 | 精品aⅴ一区二区三区 | 牛和人交xxxx欧美 | 爱做久久久久久 | 97久久国产亚洲精品超碰热 | 日韩精品无码免费一区二区三区 | 99精品无人区乱码1区2区3区 | 国产在线无码精品电影网 | 久久久久成人精品免费播放动漫 | 99精品久久毛片a片 | 在线播放亚洲第一字幕 | 亚洲第一无码av无码专区 | 国产精品久久福利网站 | 欧美性生交xxxxx久久久 | 婷婷综合久久中文字幕蜜桃三电影 | 久热国产vs视频在线观看 | 99国产欧美久久久精品 | 四虎4hu永久免费 | 久久综合给久久狠狠97色 | 国产av剧情md精品麻豆 | 国产情侣作爱视频免费观看 | 99精品无人区乱码1区2区3区 | 永久黄网站色视频免费直播 | 欧洲vodafone精品性 | 欧美野外疯狂做受xxxx高潮 | 国产9 9在线 | 中文 | 午夜不卡av免费 一本久久a久久精品vr综合 | av无码久久久久不卡免费网站 | 亚洲男女内射在线播放 | 无码任你躁久久久久久久 | 欧美日韩在线亚洲综合国产人 | 日本大香伊一区二区三区 | 日本高清一区免费中文视频 | 大肉大捧一进一出好爽视频 | 成在人线av无码免观看麻豆 | 99久久精品日本一区二区免费 | 蜜桃视频插满18在线观看 | 国产艳妇av在线观看果冻传媒 | 国语自产偷拍精品视频偷 | 国产av一区二区精品久久凹凸 | 午夜嘿嘿嘿影院 | 少妇无套内谢久久久久 | 欧美性黑人极品hd | 久久aⅴ免费观看 | 夜夜夜高潮夜夜爽夜夜爰爰 | 国产香蕉97碰碰久久人人 | аⅴ资源天堂资源库在线 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 最近中文2019字幕第二页 | 男女下面进入的视频免费午夜 | 国产精品对白交换视频 | 偷窥村妇洗澡毛毛多 | 国产成人无码av一区二区 | 少女韩国电视剧在线观看完整 | 日韩少妇内射免费播放 | 亚洲熟妇色xxxxx亚洲 | 性欧美疯狂xxxxbbbb | 熟女俱乐部五十路六十路av | 欧美三级a做爰在线观看 | 亚洲一区二区三区四区 | 在线观看国产午夜福利片 | 少妇人妻偷人精品无码视频 | 又紧又大又爽精品一区二区 | 日本xxxx色视频在线观看免费 | 无码帝国www无码专区色综合 | 免费人成在线观看网站 | 99国产欧美久久久精品 | 欧美丰满老熟妇xxxxx性 | 中文字幕无码乱人伦 | 日本熟妇人妻xxxxx人hd | 久久综合色之久久综合 | 久久熟妇人妻午夜寂寞影院 | 亚洲熟女一区二区三区 | 色情久久久av熟女人妻网站 | 久久综合狠狠综合久久综合88 | 美女扒开屁股让男人桶 | 在线精品亚洲一区二区 | 国产熟女一区二区三区四区五区 | 老司机亚洲精品影院无码 | 99久久亚洲精品无码毛片 | 欧美一区二区三区 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 又大又黄又粗又爽的免费视频 | 色欲综合久久中文字幕网 | 欧美真人作爱免费视频 | 精品久久久无码中文字幕 | 在线 国产 欧美 亚洲 天堂 | 国产精品丝袜黑色高跟鞋 | 在线观看国产一区二区三区 | 国产艳妇av在线观看果冻传媒 | 大地资源中文第3页 | 日韩精品a片一区二区三区妖精 | 久久午夜无码鲁丝片秋霞 | 日韩av激情在线观看 | 成熟女人特级毛片www免费 | www国产亚洲精品久久网站 | 国内揄拍国内精品人妻 | 日日碰狠狠躁久久躁蜜桃 | 日韩av无码中文无码电影 | 少妇邻居内射在线 | 亚洲欧美国产精品专区久久 | 激情五月综合色婷婷一区二区 | 国产亚洲精品久久久ai换 | 夜夜躁日日躁狠狠久久av | 蜜臀av在线观看 在线欧美精品一区二区三区 | 人人妻人人澡人人爽欧美精品 | 中文字幕av无码一区二区三区电影 | 亚洲va中文字幕无码久久不卡 | 日本丰满熟妇videos | 日本xxxx色视频在线观看免费 | 天天av天天av天天透 | 欧美高清在线精品一区 | 午夜精品久久久久久久久 | 国产av一区二区三区最新精品 | 色婷婷综合中文久久一本 | 男女性色大片免费网站 | 少妇人妻av毛片在线看 | 免费无码肉片在线观看 | 国产免费无码一区二区视频 | 免费无码肉片在线观看 | 国产电影无码午夜在线播放 | 无码帝国www无码专区色综合 | 2019午夜福利不卡片在线 | 日韩av无码中文无码电影 | 午夜丰满少妇性开放视频 | 波多野结衣aⅴ在线 | 精品欧洲av无码一区二区三区 | 亚洲精品一区二区三区在线观看 | 六月丁香婷婷色狠狠久久 | 精品人妻人人做人人爽 | 人妻aⅴ无码一区二区三区 | 88国产精品欧美一区二区三区 | 老头边吃奶边弄进去呻吟 | 四虎永久在线精品免费网址 | 丝袜 中出 制服 人妻 美腿 | 欧美精品国产综合久久 | 色一情一乱一伦一区二区三欧美 | 成人动漫在线观看 | 精品久久久无码中文字幕 | 精品无码国产一区二区三区av | 亚洲综合无码久久精品综合 | 国产精品久久福利网站 | 四虎永久在线精品免费网址 | 色欲人妻aaaaaaa无码 | 亚洲精品午夜无码电影网 | 青青久在线视频免费观看 | a片免费视频在线观看 | 精品无码一区二区三区的天堂 | 亚洲欧美综合区丁香五月小说 | 伊人色综合久久天天小片 | 国产舌乚八伦偷品w中 | 西西人体www44rt大胆高清 | 少妇高潮一区二区三区99 | 国产 精品 自在自线 | 国产xxx69麻豆国语对白 | 未满小14洗澡无码视频网站 | 99麻豆久久久国产精品免费 | 国产小呦泬泬99精品 | 中文字幕无码免费久久9一区9 | 免费观看激色视频网站 | 国产精品18久久久久久麻辣 | 国模大胆一区二区三区 | 日本精品高清一区二区 | 婷婷丁香六月激情综合啪 | 亚洲国产欧美国产综合一区 | 亚洲区小说区激情区图片区 | 欧美午夜特黄aaaaaa片 | 精品人妻av区 | www国产亚洲精品久久网站 | 无码帝国www无码专区色综合 | 亚洲精品久久久久avwww潮水 | 国产成人精品三级麻豆 | 纯爱无遮挡h肉动漫在线播放 | 国产成人人人97超碰超爽8 | 国产成人无码午夜视频在线观看 | 久久国产36精品色熟妇 | 性生交大片免费看l | 国产精品二区一区二区aⅴ污介绍 | 国产在线一区二区三区四区五区 | 狠狠色丁香久久婷婷综合五月 | 麻豆人妻少妇精品无码专区 | 正在播放东北夫妻内射 | 国精产品一品二品国精品69xx | 日韩精品无码免费一区二区三区 | 亚洲 激情 小说 另类 欧美 | 日韩av无码一区二区三区 | 日本xxxx色视频在线观看免费 | 大屁股大乳丰满人妻 | 天海翼激烈高潮到腰振不止 | 日日摸日日碰夜夜爽av | 亚洲欧美精品aaaaaa片 | 在线播放无码字幕亚洲 | 双乳奶水饱满少妇呻吟 | 久9re热视频这里只有精品 | 国产高潮视频在线观看 | 亚洲国产成人a精品不卡在线 | 学生妹亚洲一区二区 | 欧洲美熟女乱又伦 | 色诱久久久久综合网ywww | √天堂资源地址中文在线 | 亚洲色www成人永久网址 | 久精品国产欧美亚洲色aⅴ大片 | 曰韩少妇内射免费播放 | 亚洲色成人中文字幕网站 | 免费网站看v片在线18禁无码 | 秋霞成人午夜鲁丝一区二区三区 | 日韩精品无码一本二本三本色 | 国产人妻精品一区二区三区不卡 | 97久久国产亚洲精品超碰热 | 国产 浪潮av性色四虎 | av在线亚洲欧洲日产一区二区 | 亚洲春色在线视频 | 国产性猛交╳xxx乱大交 国产精品久久久久久无码 欧洲欧美人成视频在线 | 76少妇精品导航 | 欧美兽交xxxx×视频 | 女人和拘做爰正片视频 | 欧洲精品码一区二区三区免费看 | 中文字幕日韩精品一区二区三区 | 偷窥村妇洗澡毛毛多 | 少妇无码一区二区二三区 | 久久人人97超碰a片精品 | 露脸叫床粗话东北少妇 | 日韩少妇白浆无码系列 | 99麻豆久久久国产精品免费 | 东京无码熟妇人妻av在线网址 | 扒开双腿疯狂进出爽爽爽视频 | 成在人线av无码免观看麻豆 | 女人被男人爽到呻吟的视频 |