從Redis分散式快取實戰入手到底層原理分析、面面俱到覆蓋大廠面試考點

概述

官方說明

Redis官網 //redis.io/ 最新版本6.2.6

Redis中文官網 //www.redis.cn/ 不過中文官網的同步更新維護相對要滯後不少時間,但對於我們基礎學習完成足夠了

Redis是一個開源(BSD許可)的記憶體數據結構存儲,用作資料庫、快取和消息代理。Redis提供豐富的數據結構,如字元串、哈希、列表、集合、帶範圍查詢、點陣圖、超對數、地理空間索引和流的排序集。Redis具有內置的複製、Lua腳本、LRU驅逐、事務和不同級別的磁碟持久性,並通過Redis Sentinel和Redis Cluster的自動分區提供高可用性。

Redis使用場景有哪些?

計數器、分散式ID生成器、海量數據統計bitmap、會話快取、分散式阻塞隊列、分散式鎖、熱點數據、社交需求好友推薦、延遲隊列(sortset)等。

Redis與Mysql的部分場景比較

  • 高性能讀寫訪問,解決mysql讀寫慢的問題、緩解mysql壓力
  • 具有較豐富可描述性數據結構和可擴展性。
  • Redis有更高優勢應對訪問熱度問題,存儲熱點數據。

安裝

單機源碼安裝

#Redis單機源碼安裝非常簡單的,先下載,提取和編譯就可以拉起來使用,Redis單機一般用於開發和學習環境,生產使用的話一般都是使用Redis Sentinel或者Redis Cluster保證高可用性
wget //download.redis.io/releases/redis-6.2.6.tar.gz
tar xzf redis-6.2.6.tar.gz
cd redis-6.2.6
make && make install

image-20211009175034539

#在當前目錄下有redis的配置文件redis.conf,先修改redis.conf中的daemonize值為yes讓redis以後台程式方式運行
redis-server redis.conf
#使用redis自帶的客戶端工具redis-cli
redis-cli
#向redis寫入一個key名hello,值為world
set hello world
#讀取key名稱為hello的值
get hello
#Redis默認配置是16個資料庫,通常沒有特殊指定連接操作的是0號庫,可以通過select命令選擇庫的索引,比如可以選擇1號庫
select 1

image-20211009175720450

image-20211011143700432

Redis Cluster安裝(偽集群)

我們這裡採用在同一台上多個埠運行多個redis實例的偽集群安裝方式(當然也可以採用之前學習的docker等容器化的方式部署redis集群),同樣需要先安裝redis,可參考上面單機安裝步驟。

#創建集群目錄,放置各集群實例的配置和數據,創建六個文件夾,分別以埠號命名7000 7001 7002 7003 7004 7005六個以埠號為名字的子目錄, 稍後我們在將每個目錄中運行一個 Redis 實例
mkdir rediscluster
cd rediscluster
mkdir 7000 7001 7002 7003 7004 7005
#並將redis.conf配置文件拷貝六個目錄下conf文件夾中,修改六個redis.conf 最少配置內容,埠port的配置和目錄文件夾名稱一致,其他內容如數據文件目錄dir、bind、密碼等配置可以按照實際的情況需求進行修改
vi redis.conf
daemonize yes
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
#分別進入6個埠目錄
cd 7000
#分別啟動相應目錄下配置文件的redis實例
redis-server redis.conf
#配置集群資訊
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
redis-cli --cluster create 192.168.50.36:7000 192.168.50.36:7001 192.168.50.36:7002 192.168.50.36:7003 192.168.50.36:7004 192.168.50.36:7005 --cluster-replicas 1

image-20211011095340998

出現下面的資訊則代表集群的資訊已經配置成功

image-20211011095506148

#通過客戶端登錄redis集群
redis-cli -c -p 7000
#和上面一樣讀取鍵值驗證redis集群是否正常

image-20211011095719957

Redis功能特性

常見功能

image-20211011150001463

Redis命令

官網提供非常詳細資訊可以查閱,對於常見命令如所有數據結構讀寫操作命令都是需要熟悉的

image-20211011105511644

也可以通過官方提供客戶端help命令查閱

image-20211011105012810

Redis客戶端庫

Redis支援非常多種語言的運營,官方上列出54種程式語言庫,待黃色星號的是對應程式語言推薦的客戶端庫

image-20211011110219634

以我們Java開發技術棧來說,推薦使用Jedis(一個非常小和健全的Redis Java客戶端)、Lettuce(先進的Redis客戶端執行緒安全同步,非同步,和反應使用。支援集群、哨兵、流水線和編解碼器。後面有Lettuce官網和GitHub源碼地址,目前很多整合框架如SpringBoot都是使用Lettuce庫)、Redisson(基於Redis伺服器的分散式協調和可擴展的Java數據結構,如封裝redis的分散式鎖)。後面有時間我們專門針對Lettuce、Redisson這兩個庫實戰和原理做專門剖析。

image-20211011110329442

從Lettuce官網上就可以簡單示例,包括對於怎麼連接單機版本和集群版本,當然實際上我們更多的是使用Spring與Redis整合作為開發方式。

#如使用Maven,pom.xml加入下面依賴
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.1.5.RELEASE</version>
</dependency>
  
#如使用Gradle,build.gradle加入下面依賴
dependencies {
  compile 'io.lettuce:lettuce-core:6.1.5.RELEASE
}
import io.lettuce.core.*;

public class ConnectToRedis {

    public static void main(String[] args) {
        #Redis分為16個庫,下面使用的是0號庫
        RedisClient redisClient = RedisClient.create("redis://password@localhost:6379/0");
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        RedisCommands<String, String> syncCommands = connection.sync();
        syncCommands.set("testkey", "test string value");
        connection.close();
        redisClient.shutdown();
    }
}
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;

public class ConnectToRedisCluster {

    public static void main(String[] args) {
        // Syntax: redis://[password@]host[:port]
        // Syntax: redis://[username:password@]host[:port]
        RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7000");
        StatefulRedisClusterConnection<String, String> connection = redisClient.connect();
        System.out.println("Connected to Redis");
        connection.close();
        redisClient.shutdown();
    }
}

Redis發布/訂閱(Pub/Sub)

Redis發布訂閱(pub/sub)是一種消息通訊模式:發送者(pub)發送消息,訂閱者(sub)接收消息,客戶端訂閱到一個或多個頻道,其他客戶端發到這些頻道的消息將會被推送到所有訂閱的客戶端;發布/訂閱與key所在空間沒有關係,它不會受任何級別的干擾,包括不同資料庫索引, 發布在db 10,訂閱可以在db 1。

image-20211012092445863

  • SUBCRIBE:訂閱一個或者多個頻道。
  • PSUBCRIBE:訂閱一個或多個符合給定模式的頻道;每個模式以 * 作為匹配符,比如*itxiaoshen匹配所有以 it 開頭的頻道(news.itxiaoshen 、 sports.itxiaoshen 等等)。
  • Publish:命令用於將資訊發送到指定的頻道。
  • Pubsub:命令用於查看訂閱與發布系統狀態。
  • UNSUBCRIBE:退訂給定的一個或多個頻道的資訊。
  • PUNSUBCRIBE:退訂所有給定模式的頻道。
#客戶端訂閱執行
SUBSCRIBE devchannel testchannel
PSUBSCRIBE *itxiaoshen blog*

#發布資訊
PUBLISH testchannel hello
PUBLISH productchannel hello
PUBLISH devchannel hello
PUBLISH new.itxiaoshen hello
PUBLISH sports.itxiaoshen hello
PUBLISH sports.xiaoshen hello
PUBLISH blog.csdn hello

#查看所有通道列表
PUBSUB CHANNELS

image-20211012102449918

image-20211012102240805

image-20211012102329627

管道

Redis是一種基於客戶端-服務端模型以及請求/響應協議的TCP服務,客戶端向服務端發送一個查詢請求,並監聽Socket返回,通常是以阻塞模式,等待服務端響應。服務端處理命令,並將結果返回給客戶端。

管道一次請求/響應伺服器能實現處理新的請求即使舊的請求還未被響應。這樣就可以將多個命令發送到伺服器,而不用等待回復,最後在一個步驟中讀取該答覆。而當執行的命令較多時,這樣的一來一回的網路傳輸所消耗的時間被稱為RTT(Round Trip Time),顯而易見,如果可以將這些命令作為一個請求一次性發送給服務端,並一次性將結果返回客戶端,會節約很多網路傳輸的消耗,可以大大提升響應時間。

image-20211011163344239

大量 pipeline 應用場景可通過 Redis 腳本(Redis 版本 >= 2.6)得到更高效的處理,後者在伺服器端執行大量工作。腳本的一大優勢是可通過最小的延遲讀寫數據,讓讀、計算、寫等操作變得非常快(pipeline 在這種情況下不能使用,因為客戶端在寫命令前需要讀命令返回的結果)。 Redis 中的腳本本身也就是一種事務, 所以任何在事務里可以完成的事, 在腳本裡面也能完成。

Lua腳本

使用Lua優點

使用內置的 Lua 解釋器,可以對 Lua(Lua是一種輕量小巧的腳本語言,用標準C語言編寫並以源程式碼形式開放。其設計目的就是為了嵌入應用程式中,從而為應用程式提供靈活的擴展和訂製功能) 腳本進行求值,Redis Lua腳本適合簡單快速執行的業務,如果是複雜計算業務則會阻塞Redis server端的處理業務。

  • 減少網路開銷:可以將多個請求通過腳本的形式一次發送,減少網路時延。
  • 原子性:Redis 使用單個 Lua 解釋器以原子性(atomic)的方式執行腳本,保證 lua 腳本在處理的過程中不會被任意其它請求打斷, 這和使用MULTI/EXEC包圍的事務很類似。
  • 復用:客戶端發送的腳本會永久存在redis中,這樣其他客戶端可以復用這一腳本,而不需要使用程式碼完成相同的邏輯。

EVAL命令

  • EVAL的第一個參數是一段 Lua 5.1 腳本程式。 這段Lua腳本不需要(也不應該)定義函數。它運行在 Redis 伺服器中。
  • EVAL的第二個參數是參數的個數,後面的參數(從第三個參數),表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數可以在 Lua 中通過全局變數 KEYS 數組,用 1 為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
  • 在命令的最後,那些不是鍵名參數的附加參數 arg [arg …] ,可以在 Lua 中通過全局變數 ARGV 數組訪問,訪問的形式和 KEYS 變數類似( ARGV[1] 、 ARGV[2] ,諸如此類)
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
#使用了redis為lua內置的redis.call函數
EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 good_price 99.00 300

image-20211011181931779

SCRIPT 命令

#SCRIPT LOAD將一個腳本裝入腳本快取,但並不立即運行它
SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
#在腳本被加入到快取之後,在任何客戶端通過EVALSHA命令,可以使用腳本的SHA1校驗和來調用這個腳本。腳本可以在快取中保留無限長的時間,直到執行SCRIPT FLUSH為止
EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 good_stock 1000 600
#SCRIPT EXISTS根據給定的腳本校驗和,檢查指定的腳本是否存在於腳本快取 
SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345

image-20211011183108031

#SCRIPT FLUSH清除所有腳本快取 
SCRIPT FLUSH
#SCRIPT KILL殺死當前正在運行的腳本

image-20211011183338974

Lua腳本文件執行示例

創建mytest.lua腳本文件

--- 獲取key
local key = KEYS[1]
--- 獲取value
local val = KEYS[2]
--- 獲取一個參數
local expire = ARGV[1]
--- 如果redis找不到這個key就去插入
if redis.call("get", key) == false then
    --- 如果插入成功,就去設置過期值
    if redis.call("set", key, val) then
        --- 由於lua腳本接收到參數都會轉為String,所以要轉成數字類型才能比較
        if tonumber(expire) > 0 then
            --- 設置過期時間
            redis.call("expire", key, expire)
        end
        return true
    end
    return false
else
    return false
end
#執行mytest.lua腳本文件
redis-cli --eval mytest.lua myKey myValue , 100

image-20211011184609835

事務

定義

Redis 事務可以一次執行多個命令,將一系列的預定義命令放入隊列,執行時按照添加順序執行,redis 的事務更像是批量執行指令,有兩個重要的保證:

  • 事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。
  • 事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。

加入事務的命令只是暫時存放在隊列中,只有在執行了 exec 指令後才會被執行

命令

  • MULTI :開啟事務,redis會將後續的命令逐個放入隊列中,然後使用EXEC命令來原子化執行這個命令系列。
  • EXEC:執行事務中的所有操作命令。
  • DISCARD:取消事務,放棄執行事務塊中的所有命令。
  • WATCH:監視一個或多個key,如果事務在執行前,這個key(或多個key)被其他命令修改,則事務被中斷,不會執行事務中的任何命令。
  • UNWATCH:取消WATCH對所有key的監視。

image-20211012111925371

事務錯誤處理

使用事務時可能會遇上以下兩種錯誤:

  • 事務在執行 EXEC 之前,入隊的命令可能會出錯,比如說,命令可能會產生語法錯誤(參數數量錯誤,參數名錯誤,等等),或者其他更嚴重的錯誤,比如記憶體不足(如果伺服器使用 maxmemory 設置了最大記憶體限制的話);伺服器會對命令入隊失敗的情況進行記錄,並在客戶端調用 EXEC 命令時,拒絕執行並自動放棄這個事務。

  • 命令可能在 EXEC 調用之後失敗。舉個例子,事務中的命令可能處理了錯誤類型的鍵,比如將列表命令用在了字元串鍵上面,諸如此類。 EXEC 命令執行之後所產生的錯誤, 並沒有對它們進行特別處理: 即使事務中有某個/某些命令在執行時產生了錯誤, 事務中的其他命令仍然會繼續執行

image-20211012111217868

為什麼Redis不支援事務回滾?

多數事務失敗是由語法錯誤或者數據結構類型錯誤導致的,語法錯誤說明在命令入隊前就進行檢測的,而類型錯誤是在執行時檢測的,這些Redis為提升性能而採用這種簡單的事務,這是不同於關係型資料庫的,特別要注意區分。

WATCH監視鎖

嚴格的說Redis的命令是原子性的,而事務是非原子性的,Redis WATCH命令可以讓事務具有回滾的能力。Redis使用WATCH命令來決定事務是繼續執行還是回滾,那就需要在MULTI之前使用WATCH來監控某些鍵值對,然後使用MULTI命令來開啟事務,執行對數據結構操作的各種命令,此時這些命令入隊列。當使用EXEC執行事務時,首先會比對WATCH所監控的鍵值對,如果沒發生改變,它會執行事務隊列中的命令,提交事務;如果發生變化,將不會執行事務中的任何命令,同時事務回滾。當然無論是否回滾,Redis都會取消執行事務前的WATCH命令。在WATCH之後,MULTI之前執行UNWATCH,則事務正常提交。

image-20211012140529627

image-20211012140953999

分散式鎖

Redisson GitHub分散式鎖使用示例 //github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers

引入Redisson的依賴,然後基於Redis實現分散式鎖的加鎖與釋放鎖,實際使用中我們也會基於redisson和spring框架的整合

maven pom依賴

<!-- //mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.3</version>
</dependency>
#配置
Config config = new Config();
config.useClusterServers()
       // use "rediss://" for SSL connection
      .addNodeAddress("redis://127.0.0.1:7181");
#創建Redisson的實例
RedissonClient redisson = Redisson.create(config);

簡單鎖的示例

RLock lock = redisson.getLock("myLock");

// traditional lock method
lock.lock();

// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);

// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

簡單紅鎖的使用示例

RReadWriteLock rwlock = redisson.getReadWriteLock("myLock");

RLock lock = rwlock.readLock();
// or
RLock lock = rwlock.writeLock();

// traditional lock method
lock.lock();

// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);

// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

Distributed locks:用Redis實現分散式鎖管理器,分散式鎖在很多場景中是非常有用,官方提供一個使用Redis實現分散式鎖的Redlock演算法,這種實現比普通的單實例實現更安全,下面為各種語言基於Redlock演算法實現分散式鎖。

image-20211012085650596

image-20211012085758921

面試題

Redis分散式鎖實現思路?

image-20211012143838776

  • 自旋鎖:循環獲取鎖,類似CAS。
  • 原子性:可利用redis lua腳本的原子性。加鎖過程可以利用set nx命令SET lock_key random_value NX PX 5000,判斷key是否存在,不存在則設置key值並設置ttl時間。random_value是客戶端生成的唯一的字元串(可使用雪花演算法),NX代表只在鍵不存在時,才對鍵進行設置操作,PX設置鍵的過期時間為5000毫秒這裡random_value取值作為客戶端加鎖的時間不宜過長過短。解鎖的過程就是將Key鍵刪除,但也不能亂刪,不能說客戶端1的請求執行緒里將客戶端2的鎖給刪除掉,這時候就可以使用到random_value來實現。刪除的時候可以通過lua腳本的原子性判斷當前請求如果是對應客戶端唯一標識字元串則將key刪除。
  • 鎖的延期:設置鎖的時間比如為10秒,通過類似看門狗技術檢查key的ttl值是否快要到期,重新設置或重置ttl的時間。
  • 上述幾點主要是實現單台redis分散式鎖的核心點,至於主從和集群可以參考上述紅鎖演算法思想。

簡單談談一致性哈希演算法和Redis哈希槽?

一句話概括一致性哈希:就是普通取模哈希演算法的改良版,哈希函數計算方法不變,只不過是通過構建環狀的 Hash 空間代替普通的線性 Hash 空間。

img

數據存儲的位置是沿順時針的方向找到的環上的第一個節點,數據傾斜和節點宕機都可能會導致快取雪崩。虛擬節點,就是對原來單一的物理節點在哈希環上虛擬出幾個它的分身節點,這些分身節點稱為「虛擬節點」。打到分身節點上的數據實際上也是映射到分身對應的物理節點上,這樣一個物理節點可以通過虛擬節點的方式均勻分散在哈希環的各個部分,解決了數據傾斜問題。

redis 集群(cluster)並沒有使用一致性哈希,而是採用了哈希槽(slot)的這種概念。主要的原因是一致性哈希演算法的節點分布基於圓環,無法很好的手動控制數據分布,比如一個節點失效,把數據轉移到下一個節點,容易造成快取雪崩,而採用hash槽+副本節點失效的時候從節點自動接替,不易造成雪崩。

redis cluster 包含了16384個哈希槽,集群使用公式 CRC16(key) % 16384 來計算鍵 key 屬於哪個槽,也即是每個 key 通過計算後都會落在具體一個槽位上,而這個槽位是屬於哪個存儲節點的,則由用戶自己定義分配,集群中的每一個節點負責處理一部分哈希槽。

Redis分區方案有哪些?

  • 客戶端分區:由客戶端決定數據被存儲在哪個redis節點或者從哪個redis節點讀取,大部分客戶端都已實現了客戶端分區。
  • 代理分區:客戶端將請求發送給代理,有代理決定請求給哪些redis實例,然後根據Redis的響應結果返回給客戶端,像Twemproxy就是redis一種代理實現。
  • 查詢路由:客戶端隨機的請求到任意一個redis實例,然後由Redis將請求轉給正確的redis實例節點。而Redis Cluster實現一種混合形式的Query routing,並不是直接將請求從一個redis節點轉發到另一個redis節點,而是在客戶端直接存儲所有實例的key存儲分布資訊,所以在客戶端上就直接redirected到正確的redis節點。
  • Redis哨兵和codis也是redis高可用的解決方案。

Redis數據類型和底層數據結構的理解?

redis底層核心實現是一個雙數組,hash 數組+鏈表,通過哈希衝突解決方法如鏈表法、再哈希。

image-20211013175457411

type是約束api, object encoding是底層實現類型

image-20211013175702339

image-20211013173318803

數據類型:string、list、set、sortset、hash、hyperloglog、 stream 、geo

底層數據結構:哈希表、跳錶、雙向鏈表、壓縮列表等

  • 簡單字元串:SDS simple dynamic string 包含free len char數組 擴容為(len+addlen)*2,二進位安全、記憶體預分配、兼容C語言庫、空間換時間 sdshdr 長度 分配空間 類型 char數組,獲取到是char數組的地址,往前偏移可獲取類型進而獲取其他。

image-20211013181637100

  • 哈希表:哈希表的製作方法一般有兩種,一種是: 開放定址法,一種是 拉鏈法。redis的哈希表的使用的是拉鏈法。

image-20211013181140096

  • 雙向鏈表:

image-20211013181213164

  • 跳錶:zset是一個有序集合、排行版功能,關於時間複雜度跳錶少量數據的話誤差比較大、大量數據的話接近olog(n),zset 的長度小於128或者key的長度小於64,ziplist壓縮列表(位元組數組),大於則使用skiplist。

image-20211013180909674

  • 壓縮列表: redis的列表鍵和哈希鍵的底層實現之一。此數據結構是為了節約記憶體而開發的。和各種語言的數組類似,它是由連續的記憶體塊組成的,這樣一來,由於記憶體是連續的,就減少了很多記憶體碎片和指針的記憶體佔用,進而節約了記憶體。

    image-20211013181953340

Redis擴容時機?

Redis擴容是使用兩個哈希表分多次漸進式rehash和動態擴容機制。當used大於size擴容,排除場景包括持久化、lua事務阻塞,如果大於5size則直接擴容,翻倍擴容如4-8-16,2指數主要方便位運算,可以將取模轉為位運算,採用頭插法,當used<=size*0.1時候進行縮容;redis擴容採用漸進式rehash的方式,redis CRUD每操作一次rehash一次,每毫秒100個數組槽位。

Redis同步機制?

Redis同步機制分為全量複製和增量複製。全同步是指slave啟動時進行的初始化同步。 增量複製是指Redis運行過程中的修改同步。

  • 全同步過程如下:
    • 在slave啟動時,會向master發送一條SYNC指令。
    • master收到這條指令後,會啟動一個備份進程將所有數據寫到rdb文件中去。
    • 更新master的狀態(備份是否成功、備份時間等),然後將rdb文件內容發送給等待中的slave。
    • 注意,master並不會立即將rdb內容發送給slave。而是為每個等待中的slave註冊寫事件,當slave對應的socket可以發送數據時,再講rdb內容發送給slave。
  • 當Redis的master/slave服務啟動後,首先進行全同步。之後,所有的寫操作都在master上,而所有的讀操作都在slave上。因此寫操作需要及時同步到所有的slave上,這種同步就是部分同步。 部分同步過程如下:
    • master收到一個操作,然後判斷是否需要同步到salve。
    • 如果需要同步,則將操作記錄到aof文件中。
    • 遍歷所有的salve,將操作的指令和參數寫入到savle的回復快取中。
    • 一旦slave對應的socket發送快取中有空間寫入數據,即將數據通過socket發出去。

redis過期策略和淘汰策略、持久化機制?

  • 過期策略

    • 定時過期
    • 惰性過期。
    • 貪心策略:redis 會將每個設置了過期時間的 key 放入到一個獨立的字典中,間隔100ms隨機抽取20個key。
  • 淘汰策略:Redis官方給的警告,當記憶體不足時,Redis會根據配置的快取策略淘汰部分keys,以保證寫入成功。當無淘汰策略時或沒有找到適合淘汰的key時,Redis直接返回out of memory錯誤。

    • volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰。
    • volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰。
    • volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰。
    • allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰。
    • allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰。
    • no-enviction(驅逐):禁止驅逐數據。
  • 持久化機制

    • RDB快照(snapshot):在默認情況下, Redis 將記憶體資料庫快照保存在名字為 dump.rdb 的二進位文件中。你可以對 Redis 進行設置, 讓它在「N 秒內數據集至少有 M 個改動」這一條件被滿足時,自動保存一次數據集。
    • AOF(append-only file):快照功能並不是非常耐久(durable): 如果 Redis 因為某些原因而造成故障停機, 那麼伺服器將丟失最近寫入、且仍未保存到快照中的那些數據。Redis 增加了一種完全耐久的持久化方式: AOF 持久化,將修改的每一條指令記錄進文件你可以通過修改配置文件來打開 AOF 功能。
    appendonly yes
    
    • 混合持久化:Redis 4.0之後帶來了一個新的持久化選項,混合持久化同樣也是通過bgrewriteaof完成的,不同的是當開啟混合持久化時,fork出的子進程先將共享的記憶體副本全量的以RDB方式寫入aof文件,然後在將aof_rewrite_buf重寫緩衝區的增量命令以AOF方式寫入到文件,寫入完成後通知主進程更新統計資訊,並將新的含有RDB格式和AOF格式的AOF文件替換舊的的AOF文件。簡單的說:新的AOF文件前半段是RDB格式的全量數據後半段是AOF格式的增量數據,如下圖

    image-20211013184050697

在redis重啟的時候,載入 aof 文件進行恢複數據:先載入 rdb 內容再載入剩餘的 aof。混合持久化配置:

aof-use-rdb-preamble yes  # yes:開啟,no:關閉

說說Redis網路IO和單執行緒為何能支援高並發?

Redis基於Reactor模式開發了網路事件處理器,這個處理器被稱為文件事件處理器。它的組成結構為4部分:多個套接字、IO多路復用程式、文件事件分派器、事件處理器。因為文件事件分派器隊列的消費是單執行緒的,所以Redis才叫單執行緒模型。Redis採用網路IO多路復用技術來保證在多連接的時候,系統的高吞吐量。多路-指的是多個socket連接,復用-指的是復用一個執行緒。多路復用主要有三種技術:select,poll,epoll。epoll是最新的也是目前最好的多路復用技術。這裡「多路」指的是多個網路連接,「復用」指的是復用同一個執行緒。採用多路I/O復用技術可以讓單個執行緒高效的處理多個連接請求(盡量減少網路IO的時間消耗),且Redis在記憶體中操作數據的速度非常快(記憶體內的操作不會成為這裡的性能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。

Redis採用單執行緒為何支援高並發?

  • Redis使用的記憶體IO,不是磁碟IO,大大降低了IO時間
  • Redis單執行緒,無需去考慮多執行緒造成的死鎖問題
  • Redis單執行緒,底層網路IO模型使用多路復用epoll方式(如果內核不支援epoll,可自動切換到select或者poll,看配置資訊可進行修改)

Redis6實現的多執行緒,只是對網路IO讀寫處理做多執行緒處理,但是對命令行的操作仍然是單執行緒的。這樣即加快了IO處理效率,又保證了原子性。

img

簡單說說Redis協議?

Redis客戶端和服務端之間使用一種名為RESP(REdis Serialization Protocol)的二進位安全文本協議進行通訊,屬於請求-響應模型。

#用SET命令來舉例說明RESP協議的格式。
SET mykey "Hello"
#實際發送的請求數據:
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n
#實際收到的響應數據:
+OK\r\n

RESP設計的十分精巧,下面是一張完備的協議描述圖。

image-20211013182842980

請說說對於快取預熱、快取穿透、快取雪崩、快取擊穿、快取更新、快取降級的理解?

  • 快取穿透

    • 定義

      • 當查詢Redis中沒有的數據時,該查詢會下沉到資料庫層,同時資料庫層也沒有該數據,當這種情況大量出現或被惡意攻擊時,介面的訪問全部透過Redis訪問資料庫,而資料庫中也沒有這些數據,我們稱這種現象為”快取穿透”。快取穿透會穿透Redis的保護,提升底層資料庫的負載壓力,同時這類穿透查詢沒有數據返回也造成了網路和計算資源的浪費。

      image-20211013190004444

    • 解決方案:

      • 在介面訪問層對用戶做校驗,如介面傳參、登陸狀態、n秒內訪問介面的次數;
      • 利用布隆過濾器,將資料庫層有的數據key存儲在位數組中,以判斷訪問的key在底層資料庫中是否存在;核心思想是布隆過濾器,在redis里也有bitmap點陣圖的類似實現,布隆過濾器過濾器不能實現動態刪除,有時間可以研究下布谷鳥過濾器,是布隆過濾器增強版本。布隆過濾器有誤判率,雖然不能完全避免數據穿透的現象,但已經可以將99.99%的穿透查詢給屏蔽在Redis層了,極大的降低了底層資料庫的壓力,減少了資源浪費。
        • 基於布隆過濾器,我們可以先將資料庫中數據的key存儲在布隆過濾器的位數組中,每次客戶端查詢數據時先訪問Redis:
        • 如果Redis內不存在該數據,則通過布隆過濾器判斷數據是否在底層資料庫內;
        • 如果布隆過濾器告訴我們該key在底層庫內不存在,則直接返回null給客戶端即可,避免了查詢底層資料庫的動作;
        • 如果布隆過濾器告訴我們該key極有可能在底層資料庫記憶體在,那麼將查詢下推到底層資料庫即可;

image-20211013185803728

  • 快取擊穿

    • 定義

      • 快取擊穿和快取穿透從名詞上可能很難區分開來,它們的區別是:穿透表示底層資料庫沒有數據且快取內也沒有數據,擊穿表示底層資料庫有數據而快取內沒有數據。當熱點數據key從快取內失效時,大量訪問同時請求這個數據,就會將查詢下沉到資料庫層,此時資料庫層的負載壓力會驟增,我們稱這種現象為”快取擊穿”。
    • 解決方案

      • 延長熱點key的過期時間或者設置永不過期,如排行榜,首頁等一定會有高並發的介面;
      • 利用互斥鎖保證同一時刻只有一個客戶端可以查詢底層資料庫的這個數據,一旦查到數據就快取至Redis內,避免其他大量請求同時穿過Redis訪問底層資料庫;

      image-20211013190230849

  • 快取雪崩

    • 定義
      • 快取雪崩是快取擊穿的”大面積”版,快取擊穿是資料庫快取到Redis內的熱點數據失效導致大量並發查詢穿過redis直接擊打到底層資料庫,而快取雪崩是指Redis中大量的key幾乎同時過期,然後大量並發查詢穿過redis擊打到底層資料庫上,此時資料庫層的負載壓力會驟增,我們稱這種現象為”快取雪崩”。事實上快取雪崩相比於快取擊穿更容易發生,對於大多數公司來講,同時超大並發量訪問同一個過時key的場景的確太少見了,而大量key同時過期,大量用戶訪問這些key的幾率相比快取擊穿來說明顯更大。
    • 解決方案
      • 在可接受的時間範圍內隨機設置key的過期時間,分散key的過期時間,以防止大量的key在同一時刻過期;
      • 對於一定要在固定時間讓key失效的場景(例如每日12點準時更新所有最新排名),可以在固定的失效時間時在介面服務端設置隨機延時,將請求的時間打散,讓一部分查詢先將數據快取起來;
      • 延長熱點key的過期時間或者設置永不過期,這一點和快取擊穿中的方案一樣;

image-20211013190549941

  • 快取預熱

    • 如字面意思,當系統上線時,快取內還沒有數據,如果直接提供給用戶使用,每個請求都會穿過快取去訪問底層資料庫,如果並發大的話,很有可能在上線當天就會宕機,因此我們需要在上線前先將資料庫內的熱點數據快取至Redis內再提供出去使用,這種操作就成為”快取預熱”。
    • 快取預熱的實現方式有很多,比較通用的方式是寫個批任務,在啟動項目時或定時去觸發將底層資料庫內的熱點數據載入到快取內。
  • 快取降級

    • 快取降級是指當訪問量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的性能時,即使是有損部分其他服務,仍然需要保證主服務可用。可以將其他次要服務的數據進行快取降級,從而提升主服務的穩定性。
    • 降級的目的是保證核心服務可用,即使是有損的。如去年雙十一的時候淘寶購物車無法修改地址只能使用默認地址,這個服務就是被降級了,這裡阿里保證了訂單可以正常提交和付款,但修改地址的服務可以在伺服器壓力降低,並發量相對減少的時候再恢復。
    • 降級可以根據實時的監控數據進行自動降級也可以配置開關人工降級。是否需要降級,哪些服務需要降級,在什麼情況下再降級,取決於大家對於系統功能的取捨。
  • 快取更新

    • 快取服務(Redis)和數據服務(底層資料庫)是相互獨立且異構的系統,在更新快取或更新數據的時候無法做到原子性的同時更新兩邊的數據,因此在並發讀寫或第二步操作異常時會遇到各種數據不一致的問題。如何解決並發場景下更新操作的雙寫一致是快取系統的一個重要知識點。

    redis技術點非常多,本章主要對redis有一個全局的理解,後續有時間我們再深入理解redis內容