提升
redis、memcached 有什麼區別?
- 類型:redis 是開源內存數據結構存儲系統,用作數據庫、緩存或消息代理;memcached 是開源的高性能分佈式內存對象緩存系統,通過減少數據庫負載來提高系統性能
- 數據結構:redis 支持字符串、列表、散列、集合、有序集合、Geo、HyperLogLog、Pub/Sub 等;memcached 支持整數和字符串
- 執行速度:redis 使用線程申請內存的方式存儲數據,memcached 使用預分配的內存池,memcached 讀寫速度快於 redis
- 複製:redis 支持主從複製;memcached 不支持複製
- 模型:redis 使用單線程的 IO 複用模型;memcached 使用多線程非阻塞 IO 複用的網絡模型
- 持久化:redis 支持 AOF 和 RDB;memcached 不支持
- 集羣:redis 原生支持集羣;memcached 依靠客戶端來實現往集羣中分片寫入數據
- 刪除策略:redis 使用惰性刪除和定期刪除;memcached 使用惰性刪除
- 等。。。
redis 的線程模型是什麼?爲什麼 redis 單線程卻能支撐高併發?
Redis 內部使用文件事件處理器(File Event Handler),這個文件事件處理器是單線程的,所以 Redis 是單線程的。它採用 IO 多路複用機制同時監聽多個 socket,根據 socket 上的事件來選擇對應的事件處理器。
文件事件處理器的結構包含4個部分:
- 多個 socket
- IO 多路複用程序
- 文件事件分派器
- 事件處理器(包括:連接應答處理器、命令請求處理器、命令回覆處理器)
多個 socket 會產生不同的操作,每個操作對應不同的文件事件。IO 多路複用程序會監聽多個 socket,將產生事件的 socket 放入隊列,文件事件分派器根據隊列中的 socket 上的事件選擇對應的事件處理器。
redis 單線程高效率原因:
- 純內存操作
- 基於非阻塞 IO 多路複用機制
- 避免多線程的頻繁上下文切換
redis 的過期策略都有哪些?
過期策略
- 定時刪除
- 含義:在設置 key 的過期時間的同時,爲該 key 創建一個定時器,髒定時器在 key 過期時間來臨時,對 key 執行刪除操作
- 優點:
- 保證內存儘快釋放
- 缺點:
- key 過多時會佔用較多 CPU
- 大量定時器會影響性能
- 惰性刪除
- 含義:key 過期時不主動刪除,而是在查詢 key 時檢查是否過期(如 setnx 等設置命令也會檢查),若過期則刪除,並返回 null
- 優點:
- 刪除操作僅發生在查詢時,對 CPU 佔用較少
- 缺點:
- 當大量 key 超時卻沒有觸發惰性刪除時,可能造成內存泄露(無用數據佔用了大量內存)
- 定期刪除
- 含義:每過一段時間執行一次刪除過期 key 操作
- 優點:
- 通過限制刪除操作的時長和頻率,降低了“定時刪除”對 CPU 的佔用
- 定期刪除降低了“惰性刪除”的內存泄露概率
- 缺點:
- 比“定時刪除”佔用更多內存
- 比“惰性刪除”佔用更多 CPU
- 難點:
- 定期刪除對比“定時刪除”和“惰性刪除”有優缺點,需要根據服務器情況合理設置刪除操作的執行時長(每次刪除執行時長)和執行頻率(每次間隔多長時間進行一次刪操作)
Redis 過期策略
Redis 過期策略爲惰性刪除 + 定期刪除
- 惰性刪除流程
- 執行 get 或 setnx 等操作時,先檢查 key 是否過期
- 若過期,則刪除 key,然後執行相應操作
- 若沒過期,直接執行相應操作
- 定期刪除流程(在指定數量個庫中各自刪除不大於指定數量個過期的 key)
- 遍歷每個數據庫(配合 current_db 配置)
- 檢查當前庫中指定個數的 key(以下爲循環體,默認每個庫檢查 20 個 key)
- 若當前庫中無 key 設置過期時間,直接進入下個庫的遍歷
- 所及獲取一個設置了過期時間的 key,檢測及過期刪除
- 若定期刪除操作已達指定時長,退出當前定期刪除
- 檢查當前庫中指定個數的 key(以下爲循環體,默認每個庫檢查 20 個 key)
- 遍歷每個數據庫(配合 current_db 配置)
過期策略與持久化
- RDB:過期 key 對 RDB 沒有影響
- 從內存到 RDB 的
bgsave
操作- 持久化 key 之前,檢測是否過期,過期的 key 不加入 RDB
- 從 RDB 恢復到內存
- 過期 key 不導入數據庫(主庫)
- 從內存到 RDB 的
- AOF:過期 key 對 AOF 沒有影響
- 從內存到 AOF
- 當 key 過期且未被刪除時,該 key 的操作不會進入 AOF(因爲沒有發生修改)
- 當 key 過期且觸發刪除操作後,程序會向 AOF 追加一條刪除命令
- AOF 重寫
- 重寫時,判斷 key 是否過期,過期的 key 不會重寫到 AOF
- 從內存到 AOF
內存淘汰策略都有哪些?
Redis 在內存空間不足的時候,爲了保證命中率,就會選擇一定的數據淘汰策略。Redis5.0 後新增兩種內存淘汰機制。
# /etc/redis.conf 配置
# 最大內存
maxmemory <bytes>
# 內存淘汰策略
maxmemory-policy noeviction
- volatile-lru:從已設置過期時間的數據集中挑選最近最少使用的數據淘汰
- allkeys-lru:從全部數據集中挑選最近最少使用的數據淘汰
- volatile-random:從已設置過期時間的數據集中隨機選擇數據淘汰
- allkeys-random:從全部數據集中隨機選擇數據淘汰
- volatile-ttl:從已設置過期時間的數據集中挑選將要過期的數據淘汰
- no-eviction:禁止驅逐數據,默認。當內存不足時,新寫入操作會報錯,但正常提供讀服務
- volatile-lfu(5.x後):從已設置過期時間的數據集中挑選最近最低頻使用的數據淘汰
- 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 能承受的 QOS 約爲幾萬,受限於機器性能。Redis 高併發和高可用通常配置爲集羣模式,採用主從架構,主負責寫,並將數據複製到從節點,從負責讀。這樣可是實現水平擴展,支撐讀寫高併發。高可用則需要使用 Redis 集羣的哨兵機制。
redis 的主從複製原理能介紹一下麼?
主從剛剛連接的時候,進行全量同步;全同步結束後,進行增量同步。當然,如果有需要,slave 在任何時候都可以發起全量同步。redis 策略是:首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。