数据结构(字典,跳跃表)、使用场景(计数器、缓存、查找表、消息队列、会话缓存、分布式锁)、Redis 与 Memcached、 键的过期时间、数据淘汰策略、持久化(RDB、AOF)
1. 數據結構
1.1 字典
dictht 是一個散列表結構,使用拉鏈法保存哈希沖突的 dictEntry
/* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table.*/typedef struct dictht {dictEntry **table;unsigned long size;unsigned long sizemask;unsigned long used; } dictht;typedef struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next; } dictEntry;Redis 的字典 dict 中包含兩個哈希表 dictht,這是為了方便進行 rehash 操作。
- 在擴容時,將其中一個 dictht 上的鍵值對 rehash 到另一個 dictht 上面
- 完成之后釋放空間并交換兩個 dictht 的角色。
rehash 操作不是一次性完成,而是采用漸進方式,這是為了避免一次性執行過多的rehash 操作給服務器帶來過大的負擔。
漸進式 rehash 通過記錄 dict 的 rehashidx 完成,它從 0 開始,然后每執行一次rehash 都會遞增。
- 例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],
- 這一次會把 dict[0] 上 table[rehashidx] 的鍵值對 rehash 到 dict[1] 上,
- dict[0] 的table[rehashidx] 指向 null,并令 rehashidx++。
在 rehash 期間,每次對字典執行添加、刪除、查找或者更新操作時,都會執行一次漸進式 rehash。
采用漸進式 rehash 會導致字典中的數據分散在兩個 dictht 上,因此對字典的操作也需要到對應的 dictht 去執行。
/* Performs N steps of incremental rehashing. Returns 1 if thereare still * keys to move from the old to the new hash table, otherwise 0is returned. * *Note that a rehashing step consists in moving a bucket (thatmay have more * than one key as we use chaining) from the old to the new hashtable, however * since part of the hash table may be composed of empty spaces,it is not * guaranteed that this function will rehash even a single bucket, since it * will visit at max N*10 empty buckets in total, otherwise theamount of * work it does would be unbound and the function may block fora long time. */int dictRehash(dict *d, int n) {int empty_visits = n * 10; /* Max number of empty buckets tovisit. */if (!dictIsRehashing(d)) return 0;while (n-- && d->ht[0].used != 0) {dictEntry *de, *nextde;/* Note that rehashidx can't overflow as we are sure the re are more* elements because ht[0].used != 0 */assert(d->ht[0].size > (unsigned long) d->rehashidx);while (d->ht[0].table[d->rehashidx] == NULL) {d->rehashidx++;if (--empty_visits == 0) return 1;} de = d->ht[0].table[d->rehashidx];/* Move all the keys in this bucket from the old to the new hash HT */while (de) {uint64_t h;nextde = de->next;/* Get the index in the new hash table */h = dictHashKey(d, de->key) & d->ht[1].sizemask;de->next = d->ht[1].table[h];d->ht[1].table[h] = de;d->ht[0].used--;d->ht[1].used++;de = nextde;} d->ht[0].table[d->rehashidx] = NULL;d->rehashidx++;} /* Check if we already rehashed the whole table... */if (d->ht[0].used == 0) {zfree(d->ht[0].table);d->ht[0] = d->ht[1];_dictReset(&d->ht[1]);d->rehashidx = -1;return 0;} /* More to rehash... */return 1;}1.2?跳躍表
是有序集合的底層實現之一。
跳躍表是基于多指針有序鏈表實現的,可以看成多個有序鏈表。
在查找時,從上層指針開始查找,找到對應的區間之后再到下一層去查找。下圖演示了查找 22 的過程。
與紅黑樹等平衡樹相比,跳躍表具有以下優點:
- 插入速度非常快速,因為不需要進行旋轉等操作來維護平衡性;
- 更容易實現;
- 支持無鎖操作。
2. 使用場景
2.1 計數器
可以對 String 進行自增自減運算,從而實現計數器功能。
Redis 這種內存型數據庫的讀寫性能非常高,很適合存儲頻繁讀寫的計數量。
2.2 緩存
將熱點數據放到內存中,設置內存的最大使用量以及淘汰策略來保證緩存的命中率。
2.3 查找表
例如 DNS 記錄就很適合使用 Redis 進行存儲。
查找表和緩存類似,也是利用了 Redis 快速的查找特性。
但是查找表的內容不能失效,而緩存的內容可以失效,因為緩存不作為可靠的數據來源。
2.4 消息隊列
List 是一個雙向鏈表,可以通過 lpop 和 lpush 寫入和讀取消息。
不過最好使用 Kafka、RabbitMQ 等消息中間件。
2.5 會話緩存
在分布式場景下具有多個應用服務器,可以使用 Redis 來統一存儲這些應用服務器的會話信息。
當應用服務器不再存儲用戶的會話信息,也就不再具有狀態,一個用戶可以請求任意一個應用服務器。
2.7 分布式鎖實現
在分布式場景下,無法使用單機環境下的鎖來對多個節點上的進程進行同步。
可以使用 Reids 自帶的 SETNX 命令實現分布式鎖,除此之外,還可以使用官方提供的 RedLock 分布式鎖實現。
2.8 其它
Set 可以實現交集、并集等操作,從而實現共同好友等功能。
ZSet 可以實現有序性操作,從而實現排行榜等功能。
3. Redis 與 Memcached
兩者都是非關系型內存鍵值數據庫,主要有以下不同:
| 支持五種不同的數據類型,可以更靈活地解決問題。 | 僅支持字符串類型 |
| 兩種持久化策略:RDB 快照和 AOF 日志 | 不支持 |
| Redis Cluster 實現了分布式的支持 | 不支持(只能通過在客戶端使用一致性哈希來實現分布式存儲,這種方式在存儲和查詢時都需要先在客戶端計算一次數據所在的節點) |
| 并不是所有數據都一直存儲在內存中,可以將一些很久沒用的value 交換到磁盤 | 數據會一直在內存中; 將內存分割成特定長度的塊來存儲數據,以完全解決內存碎片的問題,但是這種方式會使得內存的利用率不高(例如塊的大小為 128 bytes,只存儲 100 bytes 的數據,那么剩下的 28 bytes 就浪費掉了) |
4.?鍵的過期時間
Redis 可以為每個鍵設置過期時間,當鍵過期時,會自動刪除該鍵。
對于散列表這種容器,只能為整個鍵設置過期時間(整個散列表) ,而不能為鍵里面的單個元素設置過期時間。
5.?數據淘汰策略
可以設置內存最大使用量,當內存使用量超出時,會施行數據淘汰策略。
Reids 具體有 6 種淘汰策略:
| 從已設置過期時間的數據集中挑選最近最少使用的數據淘汰 |
| 從已設置過期時間的數據集中挑選將要過期的數據淘汰 |
| 從已設置過期時間的數據集中任意選擇數據淘汰 |
| 從所有數據集中挑選最近最少使用的數據淘汰 |
| 從所有數據集中任意選擇數據進行淘汰 |
| 禁止驅逐數據 |
作為內存數據庫,出于對性能和內存消耗的考慮,Redis 的淘汰算法實際實現上并非針對所有 key,而是抽樣一小部分并且從中選出被淘汰的 key。
使用 Redis 緩存數據時,為了提高緩存命中率,需要保證緩存數據都是熱點數據。
可以將內存最大使用量設置為熱點數據占用的內存量,然后啟用 allkeys-lru 淘汰策略,將最近最少使用的數據淘汰。
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通過統計訪問頻率,將訪問頻率最少的鍵值對淘汰。
6. 持久化
Redis 是內存型數據庫,為了保證數據在斷電后不會丟失,需要將內存中的數據持久化到硬盤上。
6.1 RDB 持久化
將某個時間點的所有數據都存放到硬盤上。
可以將快照復制到其它服務器從而創建具有相同數據的服務器副本。
如果系統發生故障,將會丟失最后一次創建快照之后的數據。
如果數據量很大,保存快照的時間會很長。
6.2 AOF 持久化
將寫命令添加到 AOF 文件(Append Only File) 的末尾。
使用 AOF 持久化需要設置同步選項,從而確保寫命令什么時候會同步到磁盤文件上。
這是因為對文件進行寫入并不會馬上將內容同步到磁盤上,而是先存儲到緩沖區,然后由操作系統決定什么時候同步到磁盤。有以下同步選項:
| 每個寫命令都同步 |
| 每秒同步一次 |
| 讓操作系統來決定何時同步 |
?
- always 選項會嚴重減低服務器的性能;
- everysec 選項比較合適,可以保證系統崩潰時只會丟失一秒左右的數據,并且
- Redis 每秒執行一次同步對服務器性能幾乎沒有任何影響;
- no 選項并不能給服務器性能帶來多大的提升,而且也會增加系統崩潰時數據丟失的數量。
隨著服務器寫請求的增多,AOF 文件會越來越大。
Redis 提供了一種將 AOF 重寫的特性,能夠去除 AOF 文件中的冗余寫命令。
總結
以上是生活随笔為你收集整理的数据结构(字典,跳跃表)、使用场景(计数器、缓存、查找表、消息队列、会话缓存、分布式锁)、Redis 与 Memcached、 键的过期时间、数据淘汰策略、持久化(RDB、AOF)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis概述、数据类型
- 下一篇: 事务、事件(文件、时间、调度和执行)、复