Redis-提升 問題整理

提升

redis、memcached 有什麼區別?

  1. 類型:redis 是開源內存數據結構存儲系統,用作數據庫、緩存或消息代理;memcached 是開源的高性能分佈式內存對象緩存系統,通過減少數據庫負載來提高系統性能
  2. 數據結構:redis 支持字符串、列表、散列、集合、有序集合、Geo、HyperLogLog、Pub/Sub 等;memcached 支持整數和字符串
  3. 執行速度:redis 使用線程申請內存的方式存儲數據,memcached 使用預分配的內存池,memcached 讀寫速度快於 redis
  4. 複製:redis 支持主從複製;memcached 不支持複製
  5. 模型:redis 使用單線程的 IO 複用模型;memcached 使用多線程非阻塞 IO 複用的網絡模型
  6. 持久化:redis 支持 AOF 和 RDB;memcached 不支持
  7. 集羣:redis 原生支持集羣;memcached 依靠客戶端來實現往集羣中分片寫入數據
  8. 刪除策略:redis 使用惰性刪除和定期刪除;memcached 使用惰性刪除
  9. 等。。。

redis 的線程模型是什麼?爲什麼 redis 單線程卻能支撐高併發?

Redis 內部使用文件事件處理器(File Event Handler),這個文件事件處理器是單線程的,所以 Redis 是單線程的。它採用 IO 多路複用機制同時監聽多個 socket,根據 socket 上的事件來選擇對應的事件處理器。

文件事件處理器的結構包含4個部分:

  1. 多個 socket
  2. IO 多路複用程序
  3. 文件事件分派器
  4. 事件處理器(包括:連接應答處理器、命令請求處理器、命令回覆處理器)

多個 socket 會產生不同的操作,每個操作對應不同的文件事件。IO 多路複用程序會監聽多個 socket,將產生事件的 socket 放入隊列,文件事件分派器根據隊列中的 socket 上的事件選擇對應的事件處理器。

redis線程模型

redis 單線程高效率原因:

  1. 純內存操作
  2. 基於非阻塞 IO 多路複用機制
  3. 避免多線程的頻繁上下文切換

redis 的過期策略都有哪些?

參考一:Redis過期策略

過期策略

  1. 定時刪除
    • 含義:在設置 key 的過期時間的同時,爲該 key 創建一個定時器,髒定時器在 key 過期時間來臨時,對 key 執行刪除操作
    • 優點:
      • 保證內存儘快釋放
    • 缺點:
      • key 過多時會佔用較多 CPU
      • 大量定時器會影響性能
  2. 惰性刪除
    • 含義:key 過期時不主動刪除,而是在查詢 key 時檢查是否過期(如 setnx 等設置命令也會檢查),若過期則刪除,並返回 null
    • 優點:
      • 刪除操作僅發生在查詢時,對 CPU 佔用較少
    • 缺點:
      • 當大量 key 超時卻沒有觸發惰性刪除時,可能造成內存泄露(無用數據佔用了大量內存)
  3. 定期刪除
    • 含義:每過一段時間執行一次刪除過期 key 操作
    • 優點:
      • 通過限制刪除操作的時長和頻率,降低了“定時刪除”對 CPU 的佔用
      • 定期刪除降低了“惰性刪除”的內存泄露概率
    • 缺點:
      • 比“定時刪除”佔用更多內存
      • 比“惰性刪除”佔用更多 CPU
    • 難點:
      • 定期刪除對比“定時刪除”和“惰性刪除”有優缺點,需要根據服務器情況合理設置刪除操作的執行時長(每次刪除執行時長)和執行頻率(每次間隔多長時間進行一次刪操作)

Redis 過期策略

Redis 過期策略爲惰性刪除 + 定期刪除

  • 惰性刪除流程
    • 執行 get 或 setnx 等操作時,先檢查 key 是否過期
    • 若過期,則刪除 key,然後執行相應操作
    • 若沒過期,直接執行相應操作
  • 定期刪除流程(在指定數量個庫中各自刪除不大於指定數量個過期的 key)
    • 遍歷每個數據庫(配合 current_db 配置)
      • 檢查當前庫中指定個數的 key(以下爲循環體,默認每個庫檢查 20 個 key)
        • 若當前庫中無 key 設置過期時間,直接進入下個庫的遍歷
        • 所及獲取一個設置了過期時間的 key,檢測及過期刪除
        • 若定期刪除操作已達指定時長,退出當前定期刪除

過期策略與持久化

  1. RDB:過期 key 對 RDB 沒有影響
    • 從內存到 RDB 的 bgsave 操作
      • 持久化 key 之前,檢測是否過期,過期的 key 不加入 RDB
    • 從 RDB 恢復到內存
      • 過期 key 不導入數據庫(主庫)
  2. AOF:過期 key 對 AOF 沒有影響
    • 從內存到 AOF
      • 當 key 過期且未被刪除時,該 key 的操作不會進入 AOF(因爲沒有發生修改)
      • 當 key 過期且觸發刪除操作後,程序會向 AOF 追加一條刪除命令
    • AOF 重寫
      • 重寫時,判斷 key 是否過期,過期的 key 不會重寫到 AOF

內存淘汰策略都有哪些?

Redis 在內存空間不足的時候,爲了保證命中率,就會選擇一定的數據淘汰策略。Redis5.0 後新增兩種內存淘汰機制。

# /etc/redis.conf 配置
# 最大內存
maxmemory <bytes>
# 內存淘汰策略
maxmemory-policy noeviction
  1. volatile-lru:從已設置過期時間的數據集中挑選最近最少使用的數據淘汰
  2. allkeys-lru:從全部數據集中挑選最近最少使用的數據淘汰
  3. volatile-random:從已設置過期時間的數據集中隨機選擇數據淘汰
  4. allkeys-random:從全部數據集中隨機選擇數據淘汰
  5. volatile-ttl:從已設置過期時間的數據集中挑選將要過期的數據淘汰
  6. no-eviction:禁止驅逐數據,默認。當內存不足時,新寫入操作會報錯,但正常提供讀服務
  7. volatile-lfu(5.x後):從已設置過期時間的數據集中挑選最近最低頻使用的數據淘汰
  8. allkeys-lfu(5.x後):從全部數據集中挑選最近最低頻使用的數據淘汰

手寫一下LRU代碼實現?

基於 LinkedHashMap 實現

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * An LRU cache, based on <code>LinkedHashMap</code>.
 *
 * <p>
 * This cache has a fixed maximum number of elements (<code>cacheSize</code>).
 * If the cache is full and another entry is added, the LRU (least recently used) entry is dropped.
 *
 * <p>
 * This class is thread-safe. All methods of this class are synchronized.
 *
 * @author Locker [email protected]
 * @version 1.0
 */
public class LRUCache<K, V> {

    private static final float hashTableLoadFactor = 0.75f;

    private LinkedHashMap<K, V> map;
    private int cacheSize;

    /**
     * Creates a new LRU cache.
     *
     * @param cacheSize the maximum number of entries that will be kept in this cache.
     */
    public LRUCache(int cacheSize) {
        this.cacheSize = cacheSize;
        int hashTableCapacity = (int) Math.ceil(cacheSize / hashTableLoadFactor) + 1;
        map = new LinkedHashMap<K, V>(hashTableCapacity, hashTableLoadFactor, true) {
            // (an anonymous inner class)
            private static final long serialVersionUID = 1;

            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > LRUCache.this.cacheSize;
            }
        };
    }

    /**
     * Retrieves an entry from the cache.<br>
     * The retrieved entry becomes the MRU (most recently used) entry.
     *
     * @param key the key whose associated value is to be returned.
     * @return the value associated to this key, or null if no value with this key exists in the cache.
     */
    public synchronized V get(K key) {
        return map.get(key);
    }

    /**
     * Adds an entry to this cache.
     * The new entry becomes the MRU (most recently used) entry.
     * If an entry with the specified key already exists in the cache, it is replaced by the new entry.
     * If the cache is full, the LRU (least recently used) entry is removed from the cache.
     *
     * @param key   the key with which the specified value is to be associated.
     * @param value a value to be associated with the specified key.
     */
    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    /**
     * Clears the cache.
     */
    public synchronized void clear() {
        map.clear();
    }

    /**
     * Returns the number of used entries in the cache.
     *
     * @return the number of entries currently in the cache.
     */
    public synchronized int usedEntries() {
        return map.size();
    }

    /**
     * Returns a <code>Collection</code> that contains a copy of all cache entries.
     *
     * @return a <code>Collection</code> with a copy of the cache content.
     */
    public synchronized Collection<Map.Entry<K, V>> getAll() {
        return new ArrayList<>(map.entrySet());
    }

    // ---------

    public static void main(String[] args) {
        LRUCache<String, String> c = new LRUCache<>(3);
        c.put("1", "one");                           // 1
        c.put("2", "two");                           // 2 1
        c.put("3", "three");                         // 3 2 1
        c.put("4", "four");                          // 4 3 2
        assert c.get("2") != null;    // 2 4 3
        c.put("5", "five");                          // 5 2 4
        c.put("4", "second four");                   // 4 5 2

        // Verify cache content.
        assert c.usedEntries() == 3;
        assert c.get("4").equals("second four");
        assert c.get("5").equals("five");
        assert c.get("2").equals("two");

        // List cache content.
        for (Map.Entry<String, String> e : c.getAll()) {
            System.out.println(e.getKey() + " : " + e.getValue());
        }
    }
}

基於哈希表和鏈表實現

public class LRUCache {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用僞頭部和僞尾部節點
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 如果 key 存在,先通過哈希表定位,再移到頭部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,創建一個新的節點
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加進哈希表
            cache.put(key, newNode);
            // 添加至雙向鏈表的頭部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,刪除雙向鏈表的尾部節點
                DLinkedNode tail = removeTail();
                // 刪除哈希表中對應的項
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // 如果 key 存在,先通過哈希表定位,再修改 value,並移到頭部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

如何保證 redis 的高併發和高可用?

參考一:如何保證 redis 的高併發和高可用

單機 Redis 能承受的 QOS 約爲幾萬,受限於機器性能。Redis 高併發和高可用通常配置爲集羣模式,採用主從架構,主負責寫,並將數據複製到從節點,從負責讀。這樣可是實現水平擴展,支撐讀寫高併發。高可用則需要使用 Redis 集羣的哨兵機制。

redis 的主從複製原理能介紹一下麼?

參考一:Redis 主從複製原理總結

主從剛剛連接的時候,進行全量同步;全同步結束後,進行增量同步。當然,如果有需要,slave 在任何時候都可以發起全量同步。redis 策略是:首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。

redis 的哨兵原理能介紹一下麼?

參考一:Redis 哨兵機制以及底層原理深入解析

redis sentinel

redis 的持久化有哪幾種方式?不同的持久化機制都有什麼 優缺點?持久化機制具體底層是如何實現的?

如何保證緩存與數據庫的雙寫一致性?

爲什麼是刪除緩存,而不是更新緩存?

redis 的併發競爭問題是什麼?如何解決這個問題?瞭解redis 事務的 CAS 方案嗎?

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章