分布式緩存整理


分布式緩存

1 Redis和Memcached有什么區別?

  1. redis支持服務端的數據操作,Memcached需要將數據取回到客戶端修改后再set回去

  2. redis擁有更豐富的數據結構與操作api

  3. 使用簡單的key-value存儲的話,Memcached的內存利用率更高,但是如果使用hash結構的話,Redis的內存利用率更高

  4. Redis是單線程模型,Memcached可以使用多線程模型,所以在存儲小數據的時候Redis性能更高

  5. Memcached不支持原生的集群模式,但是Redis支持原生集群模式

 

redis的線程模型是什么?

單線程模型

1)文件事件處理器 File Event Handler

redis基於reactor模式開發了網絡事件處理器,這個處理器叫做文件事件處理器,file event handler。這個文件事件處理器,是單線程的,redis才叫做單線程的模型,采用IO多路復用機制同時監聽多個socket,根據socket上的事件來選擇對應的事件處理器來處理這個事件。

 

如果被監聽的socket准備好執行acceptreadwriteclose等操作的時候,跟操作對應的文件事件就會產生,這個時候文件事件處理器就會調用之前關聯好的事件處理器來處理這個事件。

 

文件事件處理器是單線程模式運行的,但是通過IO多路復用機制監聽多個socket,可以實現高性能的網絡通信模型,又可以跟內部其他單線程的模塊進行對接,保證了redis內部的線程模型的簡單性。

 

文件事件處理器的結構包含4個部分多個socketIO多路復用程序文件事件分派器事件處理器(命令請求處理器、命令回復處理器、連接應答處理器,等等)。

事件處理器包含:

  • 連接應答 處理器

    處理socket的連接請求

  • 命令請求 處理器

    處理socket的讀寫請求

  • 命令回復 處理器

    將socket的讀寫請求后的結果返回

多個socket可能並發的產生不同的操作,每個操作對應不同的文件事件,但是IO多路復用程序會監聽多個socket,但是會將socket放入一個隊列中排隊,每次從隊列中取出一個socket給事件分派器,事件分派器把socket給對應的事件處理器。

 

然后一個socket的事件處理完之后,IO多路復用程序才會將隊列中的下一個socket給事件分派器。文件事件分派器會根據每個socket當前產生的事件,來選擇對應的事件處理器來處理。

 

2)文件事件

 

當socket變得可讀時(比如客戶端對redis寫入指令,或者close操作),或者有新的可以應答的socket出現時(客戶端對redis執行connect操作),socket就會產生一個AE_READABLE事件。

 

當socket變得可寫的時候(客戶端對redis執行read操作),socket會產生一個AE_WRITABLE事件。

 

IO多路復用程序可以同時監聽AE_READABLEAE_WRITABLE兩種事件要是一個socket同時產生了AE_READABLE和AE_WRITABLE兩種事件,那么文件事件分派器優先處理AE_READABLE事件,然后才是AE_WRITABLE事件。

 

3)文件事件處理器

 

如果是客戶端要連接redis,那么會為socket關聯------------------連接應答處理器

如果是客戶端要寫數據到redis,那么會為socket關聯------------------命令請求處理器

如果是客戶端要從redis讀數據,那么會為socket關聯------------------命令回復處理器

 

4)客戶端與redis通信的一次流程

 

在redis啟動初始化的時候,redis會將連接應答處理器跟AE_READABLE事件關聯起來,接着如果一個客戶端跟redis發起連接,此時會產生一個AE_READABLE事件,然后由連接應答處理器來處理跟客戶端建立連接,創建客戶端對應的socket,同時將服務端創建的這個socket的AE_READABLE事件跟命令請求處理器關聯起來。

 

當客戶端向redis發起請求的時候(不管是讀請求還是寫請求,都一樣),首先就會在socket產生一個AE_READABLE事件,然后由對應的命令請求處理器來處理。這個命令請求處理器就會從socket中讀取請求相關數據,然后進行執行和處理。

接着redis這邊准備好了給客戶端的響應數據之后,就會將socket的AE_WRITABLE事件跟命令回復處理器關聯起來,當客戶端這邊准備好讀取響應數據時,就會在socket上產生一個AE_WRITABLE事件,會由對應的命令回復處理器來處理,就是將准備好的響應數據寫入socket,供客戶端來讀取。

 

命令回復處理器寫完之后,就會刪除這個socket的AE_WRITABLE事件和命令回復處理器的關聯關系。

 

為什么redis是單線程的但是還可以支撐高並發?

1)純內存操作

2)核心是基於非阻塞的IO多路復用機制

3)單線程反而避免了多線程的頻繁上下文切換問題

 

redis都有哪些數據類型?分別在哪些場景下使用比較合適?

由於網上有關於redis數據結構的詳細操作,這里只做簡單描述。

(1)string

這是最基本的類型了,就是普通的set和get,做簡單的kv緩存

 

(2)hash

這個是類似map的一種結構,這個一般就是可以將結構化的數據,比如一個對象(前提是這個對象沒嵌套其他的對象)給緩存在redis里,然后每次讀寫緩存的時候,可以就操作hash里的某個字段。

 

(3)list

有序列表,這個數據結構可以適用到很多場景

 

比如微博,某個大v的粉絲,就可以以list的格式放在redis里去緩存

 

key=某大v

 

value=[zhangsan, lisi, wangwu]

 

比如可以通過list存儲一些列表型的數據結構,類似粉絲列表了、文章的評論列表了之類的東西

 

比如可以通過lrange命令,就是從某個元素開始讀取多少個元素,可以基於list實現分頁查詢,這個很棒的一個功能,基於redis實現簡單的高性能分頁,可以做類似微博那種下拉不斷分頁的東西,性能高,就一頁一頁走

 

比如可以搞個簡單的消息隊列,從list頭懟進去,從list尾巴那里弄出來

 

(4)set

無序集合,自動去重

 

直接基於set將系統里需要去重的數據扔進去,自動就給去重了,如果你需要對一些數據進行快速的全局去重,你當然也可以基於jvm內存里的HashSet進行去重,但是如果你的某個系統部署在多台機器上呢?

 

得基於redis進行全局的set去重

 

可以基於set做交集、並集、差集的操作,比如交集吧,可以把A,B兩個人的粉絲列表整一個交集,看看倆人的共同好友

 

或者集合A-集合B,A的好友但是卻不是B的好友,有可能是B認識的人

 

(5)sorted set

排序的set,去重但是可以排序,寫進去的時候給一個分數,自動根據分數排序,最大的特點是有個分數可以自定義排序規則

 

比如說想根據時間對數據排序,那么可以寫入進去的時候用某個時間作為分數

 

排行榜:將每個用戶以及其對應的什么分數寫入進去,zadd board score username,接着zrevrange board 0 99,就可以獲取排名前100的用戶;zrank board username,可以看到用戶在排行榜里的排名

 

zadd board 85 zhangsan

zadd board 72 wangwu

zadd board 96 lisi

zadd board 62 zhaoliu

 

96 lisi

85 zhangsan

72 wangwu

62 zhaoliu

 

zrevrange board 0 3

 

獲取排名前3的用戶

 

96 lisi

85 zhangsan

72 wangwu

redis的過期策略都有哪些?

  • 定期刪除

    redis默認每隔100ms就隨機抽取一些設置了過期時間的key,檢查其是否過期,如果過期就刪除。

    假設redis里放了10萬個key,都設置了過期時間,你每隔幾百毫秒,就檢查10萬個key,那redis基本上就死了,cpu負載會很高的,消耗在你的檢查過期key上了。

    注意這里可不是每隔100ms就遍歷所有的設置過期時間的key,那樣就是一場性能上的災難。實際上redis是每隔100ms隨機抽取一些key來檢查和刪除的。

  • 惰性刪除

    惰性刪除了就是說在你獲取某個key的時候,redis會檢查一下 ,這個key如果設置了過期時間那么是否過期了?如果過期了此時就會刪除,不會給你返回任何東西。

    一般是配合定期刪除惰性刪除一起使用

內存淘汰機制都有哪些?

  1. no-eviction:當內存不足以容納新寫入數據時,新寫入操作會報錯

  2. allkeys-lru:當內存不足以容納新寫入數據時,在所有鍵空間中,移除最近最少使用的key這個是最常用的

  3. allkeys-random:當內存不足以容納新寫入數據時,在所有鍵空間中,隨機移除某個key

  4. volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key(這個一般不太合適)

  5. volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個key

  6. volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的key優先移除

 

LRU代碼實現

API實現

import java.util.LinkedHashMap;
import java.util.Map;
​
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
    //定義緩存的容量
    private int cacheSize;
​
    //帶參數的構造器
    LRULinkedHashMap(int cacheSize) {
        //如果accessOrder為true的話,則會把訪問過的元素放在鏈表后面,放置順序是訪問的順序
        //如果accessOrder為flase的話,則按插入順序來遍歷
        super((int) Math.ceil(cacheSize / 0.75f) + 1, 0.75f, true);
        //傳入指定的緩存最大容量
        this.cacheSize = cacheSize;
    }
​
    //實現LRU的關鍵方法,如果map里面的元素個數大於了緩存最大容量,則刪除鏈表的頂端元素
    @Override
    public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > cacheSize;
    }
​
    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        for (Map.Entry<K, V> entry : this.entrySet()) {
            stringBuilder.append(String.format("[ %s : %s ]\n", entry.getKey(), entry.getValue()));
        }
        return stringBuilder.toString();
    }
​
    //test
    public static void main(String[] args) {
        LRULinkedHashMap<String, Integer> testCache = new LRULinkedHashMap<>(3);
        testCache.put("A", 1);
        testCache.put("B", 2);
        testCache.put("C", 3);
//        System.out.println(testCache.get("B"));
//        System.out.println(testCache.get("A"));
        testCache.put("D", 4);
        testCache.put("E", 5);
        System.out.println(testCache);
    }
}

 

原生實現

import java.util.HashMap;
​
/**
 * 使用cache和鏈表實現緩存
 */
public class LRU2<K, V> {
    /**
     * 最大緩存大小
     */
    private final int MAX_CACHE_SIZE;
    /**
     * 頭結點
     */
    private Entry<K, V> head;
    /**
     * 尾節點
     */
    private Entry<K, V> tail;
​
    /**
     * 緩存map
     */
    private HashMap<K, Entry<K, V>> cacheMap;
​
    public LRU2(int cacheSize) {
        MAX_CACHE_SIZE = cacheSize;
        cacheMap = new HashMap<>();
    }
​
    public void put(K key, V value) {
        Entry<K, V> entry = getEntry(key);
        //如果是新添加的元素
        if (entry == null) {
            //如果緩存大小>=最大指定大小
            if (cacheMap.size() >= MAX_CACHE_SIZE) {
                //從緩存中刪除尾節點的元素
                cacheMap.remove(tail.key);
                //去掉尾節點
                removeTail();
            }
            //創建新節點
            entry = new Entry<>();
            entry.key = key;
            entry.value = value;
            //將每次新增的節點都放到頭結點
            moveToHead(entry);
            //並且添加到緩存中
            cacheMap.put(key, entry);
        } else {
            //如果是緩存中已經存在的元素 則直接賦值
            entry.value = value;
            //將每次新增的節點都放到頭結點
            moveToHead(entry);
        }
    }
​
    /**
     * 根據鍵獲取值
     *
     * @param key
     * @return
     */
    public V get(K key) {
        Entry<K, V> entry = getEntry(key);
        if (entry == null) {
            return null;
        }
        //將每次操作過的節點放到頭結點位置
        moveToHead(entry);
        return entry.value;
    }
​
    /**
     * 移除當前Key
     *
     * @param key
     */
    public void remove(K key) {
        //獲取當前Key的Value
        Entry<K, V> entry = getEntry(key);
        //如果value存在
        if (entry != null) {
            //如果value為頭結點
            if (entry == head) {
                Entry<K, V> next = head.next;
                //讓當前頭結點的下一個節點為空
                head.next = null;
                //讓之前頭結點的下一個節點為新的頭結點
                head = next;
                //讓當前頭結點的前一個節點為空
                head.pre = null;
            } else if (entry == tail) {//如果當前需要刪除的節點為尾節點
                Entry<K, V> prev = tail.pre;
                //讓當前尾節點的前一個節點為空
                tail.pre = null;
                //讓舊的尾節點的前一個節點為新的尾節點
                tail = prev;
                //讓當前尾節點的下一個節點為空
                tail.next = null;
            } else {//如果當前需要刪除的節點為中間節點
                //直接讓當前刪除的節點的上一個節點的next指向當前刪除的節點的下一個節點
                entry.pre.next = entry.next;
                //直接讓當前刪除的節點的下一個節點的pre指向當前刪除的節點的上一個節點
                entry.next.pre = entry.pre;
            }
            //從緩存map中刪除該鍵
            cacheMap.remove(key);
        }
    }
​
    /**
     * 移除尾節點
     */
    private void removeTail() {
        //如果尾節點不為空
        if (tail != null) {
            //讓尾節點的前一個節點=prev
            Entry<K, V> prev = tail.pre;
            //如果尾節點的前一個節點為空 說明整個鏈表就一個節點
            if (prev == null) {
                //讓頭尾節點都為空
                head = null;
                tail = null;
            } else {
                //如果尾節點的前一個節點不為空
                //讓尾節點與前一個節點斷開
                tail.pre = null;
                //讓prev為當前鏈表的尾節點
                tail = prev;
                //讓prev的下一個節點為空(之前引用着已經刪除了的尾節點)
                tail.next = null;
            }
        }
    }
​
    /**
     * 將參數節點放到頭結點
     * 此時置於尾端的節點就是訪問次數最少的節點 應該被淘汰掉
     * 符合LRU算法思想
     *
     * @param entry 參數節點
     */
    private void moveToHead(Entry<K, V> entry) {
        //如果當前節點已經為頭結點 直接結束
        if (entry == head) {
            return;
        }
        //如果當前頭尾節點都為空 說明整個鏈表為空
        if (head == null || tail == null) {
            //即讓頭尾節點都為當前節點entry
            head = tail = entry;
            return;
        }
​
        //如果當前節點的前一個節點不為空
        if (entry.pre != null) {
            //讓當前節點的前一個節點的next指向當前節點的下一個節點
            // 比如 A->-B->C 現在將B移到頭結點 那么B與A C斷開 即為 A->C
            entry.pre.next = entry.next;
        }
        //如果當前節點的下一個節點不為空
        if (entry.next != null) {
            //讓當前節點的下一個節點的pre指向當前節點的前一個節點
            entry.next.pre = entry.pre;
        }
        //如果當前節點即為尾節點
        if (entry == tail) {
            //讓prev為尾節點的前一個節點
            Entry<K, V> prev = entry.pre;
            //如果尾節點的前一個節點不為空
            if (prev != null) {
                //讓尾節點的前一個節點為空
                tail.pre = null;
                //讓prev為當前尾節點
                tail = prev;
                //讓prev的下一個節點為空
                tail.next = null;
            }
        }
​
        entry.next = head;
        head.pre = entry;
        entry.pre = null;
        head = entry;
    }
​
    private Entry<K, V> getEntry(K key) {
        return cacheMap.get(key);
    }
​
    private static class Entry<K, V> {
        Entry<K, V> pre;
        Entry<K, V> next;
        K key;
        V value;
    }
​
    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        Entry<K, V> entry = head;
        stringBuilder.append("head >> ");
        while (entry != null) {
            stringBuilder.append(String.format("%s:%s ", entry.key, entry.value));
            entry = entry.next;
        }
        stringBuilder.append(" >> tail");
        return stringBuilder.toString();
    }
​
    public static void main(String[] args) {
        LRU2<Integer, Integer> lru2 = new LRU2<>(5);
        lru2.put(1, 1);
        System.out.println(lru2);
        lru2.put(2, 2);
        System.out.println(lru2);
        lru2.put(3, 3);
        System.out.println(lru2);
        lru2.get(1);
        System.out.println(lru2);
        lru2.put(4, 4);
        lru2.put(5, 5);
        lru2.put(6, 6);
        System.out.println(lru2);
    }
}

 

如何保證Redis的高並發(分散流量)和高可用(一主多從+哨兵)?

redis高並發:主從架構,一主多從,一般來說,很多項目其實就足夠了,單主用來寫入數據,單機幾萬QPS,多從用來查詢數據,多個從實例可以提供每秒10萬的QPS

 

redis高並發的同時,還需要容納大量的數據:一主多從,每個實例都容納了完整的數據,比如redis主就10G的內存量,其實你就最對只能容納10g的數據量。如果你的緩存要容納的數據量很大,達到了幾十g,甚至幾百g,或者是幾t,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒幾十萬的讀寫並發。

 

redis高可用:如果你做主從架構部署,其實就是加上哨兵就可以了,就可以實現,任何一個實例宕機,自動會進行主備切換。

redis的主從復制原理

  1. redis replication的核心機制

    (1)redis采用異步方式復制數據到slave節點,不過redis 2.8開始,slave node會周期性地確認自己每次復制的數據量 (2)一個master node是可以配置多個slave node (3)slave node也可以連接其他的slave node (4)slave node在做復制的時候,是不會block master node的正常工作的 (5)slave node在做復制的時候,也不會block對自己的查詢操作,它會用舊的數據集來提供服務; 但是復制完成的時候,需要刪除舊數據集,加載新數據集,這個時候就會暫停對外服務了 (6)slave node主要用來進行橫向擴容,做讀寫分離擴容的slave node可以提高讀的吞吐量(將大量的請求分散到不同的slave節點上 提高了redis主從架構的吞吐量 單機redis的吞吐量沒變)

     

  2. master持久化對於主從架構的安全保障的意義

    如果采用了主從架構,那么建議必須開啟master node的持久化!(AOF/RDB)

    不建議用slave node作為master node的數據熱備,因為那樣的話,如果你關掉master的持久化,可能在master宕機重啟的時候數據是空的,然后可能一經過復制,salve node數據也丟了

    master -> RDB和AOF都關閉了 -> 全部在內存中

    master宕機,重啟,是沒有本地數據可以恢復的,然后就會直接認為自己IDE數據是空的

    master就會將空的數據集同步到slave上去,所有slave的數據全部清空

    100%的數據丟失

    master節點,必須要使用持久化機制

    第二個,master的各種備份方案,要不要做,萬一說本地的所有文件丟失了; 從備份中挑選一份rdb去恢復master; 這樣才能確保master啟動的時候,是有數據的

    即使采用了后續講解的高可用機制,slave node可以自動接管master node,但是也可能sentinal還沒有檢測到master failure,master node就自動重啟了,還是可能導致上面的所有slave node數據清空故障

  3. 主從架構的核心原理

    啟動一個slave node的時候,它會發送一個PSYNC命令給master node

    如果這是slave node重新連接master node,那么master node僅僅會復制給slave部分缺少的數據; 否則如果是slave node第一次連接master node,那么會觸發一次full resynchronization

    開始full resynchronization的時候,master會啟動一個后台線程,開始生成一份RDB快照文件,同時還會將從客戶端收到的所有寫命令緩存在內存中。RDB文件生成完畢之后,master會將這個RDB發送給slave,slave會先寫入本地磁盤,然后再從本地磁盤加載到內存中。然后master會將內存中緩存的寫命令發送給slave,slave也會同步這些數據。

    slave node如果跟master node有網絡故障,斷開了連接,會自動重連。master如果發現有多個slave node都來重新連接,僅僅會啟動一個rdb save操作,用一份數據服務所有slave node。

  4. 主從復制的斷點續傳

    redis 2.8開始,就支持主從復制的斷點續傳,如果主從復制過程中,網絡連接斷掉了,那么可以接着上次復制的地方,繼續復制下去,而不是從頭開始復制一份

    master node會在內存中常見一個backlog,master和slave都會保存一個replica offset還有一個master id,offset就是保存在backlog中的。如果master和slave網絡連接斷掉了,slave會讓master從上次的replica offset開始繼續復制

    但是如果沒有找到對應的offset,那么就會執行一次resynchronization

  5. 無磁盤化復制

    master在內存中直接創建rdb,然后發送給slave,不會在自己本地落地磁盤了

    repl-diskless-sync repl-diskless-sync-delay,等待一定時長再開始復制,因為要等更多slave重新連接過來

  6. 過期key處理

    slave不會過期key,只會等待master過期key。如果master過期了一個key,或者通過LRU淘汰了一個key,那么會模擬一條del命令發送給slave。

  7. 什么是99.99%高可用?

    架構上,高可用性,99.99%的高可用性

    講的學術,99.99%,公式,系統可用的時間 / 系統故障的時間,365天,在365天 * 99.99%的時間內,你的系統都是可以對外提供服務的,那就是高可用性

     

redis的哨兵原理

  1. 哨兵的介紹

    sentinal,中文名是哨兵

    哨兵是redis集群架構中非常重要的一個組件,主要功能如下

    (1)集群監控,負責監控redis master和slave進程是否正常工作 (2)消息通知,如果某個redis實例有故障,那么哨兵負責發送消息作為報警通知給管理員 (3)故障轉移,如果master node掛掉了,會自動轉移到slave node上 (4)配置中心,如果故障轉移發生了,通知client客戶端新的master地址

    哨兵本身也是分布式的,作為一個哨兵集群去運行,互相協同工作

    (1)故障轉移時,判斷一個master node是宕機了,需要大部分的哨兵都同意才行,涉及到了分布式選舉的問題 (2)即使部分哨兵節點掛掉了,哨兵集群還是能正常工作的,因為如果一個作為高可用機制重要組成部分的故障轉移系統本身是單點的,那就很坑爹了

    目前采用的是sentinal 2版本,sentinal 2相對於sentinal 1來說,重寫了很多代碼,主要是讓故障轉移的機制和算法變得更加健壯和簡單

  2. 哨兵的核心知識

    (1)哨兵至少需要3個實例,來保證自己的健壯性 (2)哨兵 + redis主從的部署架構,是不會保證數據零丟失的,只能保證redis集群的高可用性 (3)對於哨兵 + redis主從這種復雜的部署架構,盡量在測試環境和生產環境,都進行充足的測試和演練

  3. 為什么redis哨兵集群只有2個節點無法正常工作?

    哨兵集群必須部署2個以上節點

    如果哨兵集群僅僅部署了個2個哨兵實例

    Configuration: quorum = 1

    master1宕機,slave1和slave2中只要有1個哨兵認為master1宕機就可以還行切換,同時slave1和slave2中會選舉出一個哨兵來執行故障轉移

    同時這個時候,需要majority,也就是大多數哨兵都是運行的,2個哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2個哨兵都運行着,就可以允許執行故障轉移

    但是如果整個master1和slave1運行的機器宕機了,那么哨兵只有1個了,此時就沒有majority來允許執行故障轉移,雖然另外一台機器還有一個R1,但是故障轉移不會執行

  4. 經典的3節點哨兵集群

    Configuration: quorum = 2

    如果master1所在機器宕機了,那么三個哨兵還剩下2個,slave2和slave3可以一致認為maste1r宕機,然后選舉出一個來執行故障轉移

    同時3個哨兵的majority是2,所以還剩下的2個哨兵運行着,就可以允許執行故障轉移

Redis持久化機制

我們已經知道對於一個企業級的redis架構來說,持久化是不可減少的

企業級redis集群架構:海量數據、高並發、高可用

持久化主要是做災難恢復,數據恢復,也可以歸類到高可用的一個環節里面去

比如你redis整個掛了,然后redis就不可用了,你要做的事情是讓redis變得可用,盡快變得可用

重啟redis,盡快讓它對外提供服務,但是就像上一講說,如果你沒做數據備份,這個時候redis啟動了,也不可用啊,數據都沒了

很可能說,大量的請求過來,緩存全部無法命中,在redis里根本找不到數據,這個時候就死定了,緩存雪崩問題,所有請求,沒有在redis命中,就會去mysql數據庫這種數據源頭中去找,一下子mysql承接高並發,然后就掛了

mysql掛掉,你都沒法去找數據恢復到redis里面去,redis的數據從哪兒來?從mysql來。。。

具體的完整的緩存雪崩的場景,還有企業級的解決方案,到后面講

如果你把redis的持久化做好,備份和恢復方案做到企業級的程度,那么即使你的redis故障了,也可以通過備份數據,快速恢復,一旦恢復立即對外提供服務

redis的持久化,跟高可用,是有關系的,企業級redis架構中去講解

redis持久化:RDBAOF


1、RDB和AOF兩種持久化機制的介紹(RDB周期性持久數據/AOF持續記錄寫命令)

RDB持久化機制,對redis中的數據執行周期性的持久化

AOF機制對每條寫入命令作為日志,以append-only的模式寫入一個日志文件中,在redis重啟的時候,可以通過回放AOF日志中的寫入指令來重新構建整個數據集

如果我們想要redis僅僅作為純內存的緩存來用,那么可以禁止RDB和AOF所有的持久化機制

通過RDB或AOF,都可以將redis內存中的數據給持久化到磁盤上面來,然后可以將這些數據備份到別的地方去,比如說阿里雲,雲服務

如果redis掛了,服務器上的內存和磁盤上的數據都丟了,可以從雲服務上拷貝回來之前的數據,放到指定的目錄中,然后重新啟動redis,redis就會自動根據持久化數據文件中的數據,去恢復內存中的數據,繼續對外提供服務

如果同時使用RDB和AOF兩種持久化機制,那么在redis重啟的時候,會使用AOF來重新構建數據,因為AOF中的數據更加完整


2、RDB持久化機制的優點(適合冷備/恢復數據更快)

(1)RDB會生成多個數據文件,每個數據文件都代表了某一個時刻中redis的數據,這種多個數據文件的方式,非常適合做冷備,可以將這種完整的數據文件發送到一些遠程的安全存儲上去,比如說Amazon的S3雲服務上去,在國內可以是阿里雲的ODPS分布式存儲上,以預定好的備份策略來定期備份redis中的數據

(2)RDB對redis對外提供的讀寫服務,影響非常小,可以讓redis保持高性能,因為redis主進程只需要fork一個子進程,讓子進程執行磁盤IO操作來進行RDB持久化即可

(3)相對於AOF持久化機制來說,直接基於RDB數據文件來重啟和恢復redis進程,更加快速


3、RDB持久化機制的缺點(宕機可能丟失間隔時間內的產生的數據/生成RDB文件過大的時候會影響服務正常提供)

(1)如果想要在redis故障時,盡可能少的丟失數據,那么RDB沒有AOF好。一般來說,RDB數據快照文件,都是每隔5分鍾,或者更長時間生成一次,這個時候就得接受一旦redis進程宕機,那么會丟失最近5分鍾的數據

(2)RDB每次在fork子進程來執行RDB快照數據文件生成的時候,如果數據文件特別大,可能會導致對客戶端提供的服務暫停數毫秒,或者甚至數秒


4、AOF持久化機制的優點(數據不易丟失/寫入AOF文件快速/AOF文件智能壓縮重寫/靈活控制AOF文件)

(1)AOF可以更好的保護數據不丟失,一般AOF會每隔1秒,通過一個后台線程執行一次fsync操作,最多丟失1秒鍾的數據

(2)AOF日志文件以append-only模式寫入,所以沒有任何磁盤尋址的開銷,寫入性能非常高,而且文件不容易破損,即使文件尾部破損,也很容易修復

(3)AOF日志文件即使過大的時候,出現后台重寫操作,也不會影響客戶端的讀寫。因為在rewrite log的時候,會對其中的指導進行壓縮,創建出一份需要恢復數據的最小日志出來。再創建新日志文件的時候,老的日志文件還是照常寫入。當新的merge后的日志文件ready的時候,再交換新老日志文件即可。

(4)AOF日志文件的命令通過非常可讀的方式進行記錄,這個特性非常適合做災難性的誤刪除的緊急恢復。比如某人不小心用flushall命令清空了所有數據只要這個時候后台rewrite還沒有發生,那么就可以立即拷貝AOF文件,將最后一條flushall命令給刪了,然后再將該AOF文件放回去,就可以通過恢復機制,自動恢復所有數據


5、AOF持久化機制的缺點(AOF文件較大/寫命令性能降低)

(1)對於同一份數據來說,AOF日志文件通常比RDB數據快照文件更大

(2)AOF開啟后,支持的寫QPS會比RDB支持的寫QPS低因為AOF一般會配置成每秒fsync一次日志文件,當然,每秒一次fsync,性能也還是很高的

(3)以前AOF發生過bug,就是通過AOF記錄的日志,進行數據恢復的時候,沒有恢復一模一樣的數據出來。所以說,類似AOF這種較為復雜的基於命令日志/merge/回放的方式,比基於RDB每次持久化一份完整的數據快照文件的方式,更加脆弱一些,容易有bug。不過AOF就是為了避免rewrite過程導致的bug,因此每次rewrite並不是基於舊的指令日志進行merge的,而是基於當時內存中的數據進行指令的重新構建,這樣健壯性會好很多。


6、RDB和AOF到底該如何選擇(配合使用)

(1)不要僅僅使用RDB,因為那樣會導致你丟失很多數據

(2)也不要僅僅使用AOF,因為那樣有兩個問題,第一,你通過AOF做冷備,沒有RDB做冷備,來的恢復速度更快; 第二,RDB每次簡單粗暴生成數據快照,更加健壯,可以避免AOF這種復雜的備份和恢復機制的bug

(3)綜合使用AOF和RDB兩種持久化機制,用AOF來保證數據不丟失,作為數據恢復的第一選擇; 用RDB來做不同程度的冷備,在AOF文件都丟失或損壞不可用的時候,還可以使用RDB來進行快速的數據恢復

redis集群模式的工作原理

redis cluster

支撐N個redis master node,每個master node都可以掛載多個slave node

讀寫分離的架構,對於每個master來說,寫就寫到master,然后讀就從mater對應的slave去讀

高可用,因為每個master都有salve節點,那么如果mater掛掉,redis cluster這套機制,就會自動將某個slave切換成master

redis cluster(多master + 讀寫分離 + 高可用)

僅高可用=單master+多slave+哨兵, master負責寫 ,slave負責讀 ,master同步數據到slave

高可用+高並發=(n乘master + nk乘slave)redis集群+哨兵

多個master之間使用一致性hash或者redis slots來分散key的存儲節點 , 然后各自同步到其對應的slave節點上,達到橫向擴容

n>1 k>1

n為master的個數

k為一個master對應的k個slave

我們只要基於redis cluster去搭建redis集群即可,不需要手工去搭建replication復制+主從架構+讀寫分離+哨兵集群+高可用

 

如果你的數據量很少,主要是承載高並發高性能的場景,比如你的緩存一般就幾個G,單機足夠了

replication,一個mater,多個slave,要幾個slave跟你的要求的讀吞吐量有關系,然后自己搭建一個sentinal集群,去保證redis主從架構的高可用性,就可以了

redis cluster,主要是針對海量數據+高並發+高可用的場景,海量數據,如果你的數據量很大,那么建議就用redis cluster

在集群模式下,redis的key是如何尋址的?

  1. hash取模尋址

  2. 一致性hash算法(自動緩存遷移)+虛擬節點(自動負載均衡)

  3. redis cluster的hash slot算法

    redis cluster有固定的16384個hash slot,對每個key計算CRC16值,然后對16384取模,可以獲取key對應的hash slot

    redis cluster中每個master都會持有部分slot,比如有3個master,那么可能每個master持有5000多個hash slot

    hash slot讓node的增加和移除很簡單,增加一個master,就將其他master的hash slot移動部分過去,減少一個master,就將它的hash slot移動到其他master上去

    移動hash slot的成本是非常低的

    客戶端的api,可以對指定的數據,讓他們走同一個hash slot,通過hash tag來實現

了解什么是redis的雪崩和穿透?

  1. redis雪崩

    某一瞬間大量的鍵集體過期或由於機器宕機后重啟導致大量內存中的數據丟失 導致某一時刻的大量請求直接打到數據庫層

    解決方法:可以使用本地ehcache緩存作為二級緩存+hystrix做限流降級處理 當Redis緩存中不存在就去本地緩存中查詢一下,這一步驟可以分擔一部分壓力 如果此時壓力還是很大 就啟用hystrix限流 讓眾多請求中只有一部分的請求繼續請求,其余的請求走降級之后的方法,比如返回友好的提示信息告訴用戶服務端繁忙請稍后再試.

  2. redis穿透(惡意攻擊)

    非法用戶大量請求緩存DB不存在的數據 比如根據根據訂單號查詢訂單的時候 查詢條件全是負數 這樣一來 大量的請求就會直接穿過Redis直接打到數據庫上 造成數據庫壓力激增

    解決方法:將不存在的鍵存入redis 值設為null或者某個標記符號 並設置過期時間 這樣一來 在過期時間內再次請求就會直接返回

  3. redis擊穿

    某一時刻大量請求某一個鍵(比如電商網站中的爆款),假如該鍵即將過期,那么在過期后的一瞬間,大量請求就會直接打到數據庫

    解決方法:過期后通過對該鍵進行加鎖 在加鎖->從數據庫中查詢->將結果放入緩存中->返回結果->解鎖這一過程中,其余的請求等待並且間隔重試 等待時間不會很久 一旦加鎖解鎖這一過程結束 程序又恢復到該鍵未過期時的狀態 還有一種解決方法就是將該鍵設為永不過期

redis崩潰之后會怎么樣?

redis崩潰之后大量請求直接打到數據庫

數據庫承載不了那么大的壓力就會崩潰

導致整個系統全盤崩潰

系統該如何應對這種情況?

事前:redis高可用,主從+哨兵,redis cluster,避免全盤崩潰

事中:本地ehcache緩存 + hystrix限流&降級,避免MySQL被打死

事后:redis持久化,快速恢復緩存數據

 

緩存與數據庫的雙寫一致性

Cache Aside Pattern

  • 查詢先從緩存中查 如果查不到再去數據庫里查 將結果加到緩存中 然后返回結果

  • 更新先刪除緩存 然后更新數據庫

 

redis的並發競爭問題

多個系統對同一個鍵進行操作,如果沒有操作順序的話會導致最終該鍵的值不一樣

如何解決這個問題?

使用分布式鎖

Redis事務的CAS方案

MULTI:     開啟一個事務,類比於mysql的openSession
EXEC:      提交一個事務,類比於mysql的commit
DISCARD:    取消一個事務,類比於mysql的rollback
WATCH:    監控一個key,與redis事務機制結合使用,形成原子鎖

watch指令在redis事物中提供了CAS的行為。為了檢測被watch的keys在是否有多個clients同時改變引起沖突,這些keys將會被監控。如果至少有一個被監控的key在執行exec命令前被修改,整個事物將會回滾,不執行任何動作,從而保證原子性操作,並且執行exec會得到null的回復。

生產環境中的redis是怎么部署的?

redis cluster,10台機器,5台機器部署了redis主實例,另外5台機器部署了redis的從實例,每個主實例掛了一個從實例,5個節點對外提供讀寫服務,每個節點的讀寫高峰qps可能可以達到每秒5萬,5台機器最多是25萬讀寫請求/s。

 

機器是什么配置?32G內存+8核CPU+1T磁盤,但是分配給redis進程的是10g內存,一般線上生產環境,redis的內存盡量不要超過10g,超過10g可能會有問題。

 

5台機器對外提供讀寫,一共有50g內存。

 

因為每個主實例都掛了一個從實例,所以是高可用的,任何一個主實例宕機,都會自動故障遷移,redis從實例會自動變成主實例繼續提供讀寫服務

 

你往內存里寫的是什么數據?每條數據的大小是多少?商品數據,每條數據是10kb。100條數據是1mb,10萬條數據是1g。常駐內存的是200萬條商品數據,占用內存是20g,僅僅不到總內存的50%。

 

目前高峰期每秒就是3500左右的請求量

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM