資料庫-Redis
春宵一刻值千金,花有清香月有陰,
簡介:資料庫-Redis,
一、概述
Windows &Mac 安裝Redis 參考鏈接:https://www.cnblogs.com/taojietaoge/p/11010704.html
Redis 是速度非常快的非關系型(NoSQL)記憶體鍵值資料庫,可以存盤鍵和五種不同型別的值之間的映射,
鍵的型別只能為字串,值支持五種資料型別:字串、串列、集合、散串列、有序集合,
Redis 支持很多特性,例如將記憶體中的資料持久化到硬碟中,使用復制來擴展讀性能,使用分片來擴展寫性能,
二、資料型別
| 資料型別 | 可以存盤的值 | 操作 |
|---|---|---|
| STRING | 字串、整數或者浮點數 |
對整個字串或者字串的其中一部分執行操作 對整數和浮點數執行自增或者自減操作 |
| LIST | 串列 |
從兩端壓入或者彈出元素 對單個或者多個元素進行修剪, 只保留一個范圍內的元素 |
| SET | 無序集合 |
添加、獲取、移除單個元素 檢查一個元素是否存在于集合中 計算交集、并集、差集 從集合里面隨機獲取元素 |
| HASH | 包含鍵值對的無序散串列 |
添加、獲取、移除單個鍵值對 獲取所有鍵值對 檢查某個鍵是否存在 |
| ZSET | 有序集合 |
添加、獲取、洗掉元素 根據分值范圍或者成員來獲取元素 計算一個鍵的排名 |
STRING

1 Taojie:~ apple$ redis-cli
2 127.0.0.1:6379> set hello world
3 OK
4 127.0.0.1:6379> get hello world
5 (error) ERR wrong number of arguments for 'get' command
6 127.0.0.1:6379> get hello
7 "world"
8 127.0.0.1:6379> del hello
9 (integer) 1
10 127.0.0.1:6379> get hello
11 (nil)
View Code

LIST

1 127.0.0.1:6379> rpush list-key item
2 (integer) 1
3 127.0.0.1:6379> rpush list-key item2
4 (integer) 2
5 127.0.0.1:6379> rpush list-key item
6 (integer) 3
7 127.0.0.1:6379> lrange list-key 0 -1
8 1) "item"
9 2) "item2"
10 3) "item"
11 127.0.0.1:6379> lindex list-key 1
12 "item2"
13 127.0.0.1:6379> lpop list-key
14 "item"
15 127.0.0.1:6379> lrange list-key 0 -1
16 1) "item2"
17 2) "item"
View Code

SET

1 Last login: Mon Jul 26 19:09:35 on ttys001
2 Taojie:~ apple$ redis-cli
3 127.0.0.1:6379> sadd set-key item
4 (integer) 1
5 127.0.0.1:6379> sadd set-key item2
6 (integer) 1
7 127.0.0.1:6379> sadd set-key item3
8 (integer) 1
9 127.0.0.1:6379> sadd set-key item
10 (integer) 0
11 127.0.0.1:6379> smembers set-key
12 1) "item3"
13 2) "item2"
14 3) "item"
15 127.0.0.1:6379> sismember set-key item4
16 (integer) 0
17 127.0.0.1:6379> set-key item
18 (error) ERR unknown command `set-key`, with args beginning with: `item`,
19 127.0.0.1:6379> sismember set-key item
20 (integer) 1
21 127.0.0.1:6379> srem set-key item2
22 (integer) 1
23 127.0.0.1:6379> srem set-key item2
24 (integer) 0
25 127.0.0.1:6379> smembers set-key
26 1) "item3"
27 2) "item"
28 127.0.0.1:6379>
View Code

HASH

1 Taojie:~ apple$ redis-cli
2 127.0.0.1:6379> hset hash-key sub-key1 value1
3 (integer) 1
4 127.0.0.1:6379> hset hash-key sub-key2 value2
5 (integer) 1
6 127.0.0.1:6379> hset hash-key sub-key1 value1
7 (integer) 0
8 127.0.0.1:6379> hgetall hash-key
9 1) "sub-key1"
10 2) "value1"
11 3) "sub-key2"
12 4) "value2"
13 127.0.0.1:6379> hdel hash-key sub-key2
14 (integer) 1
15 127.0.0.1:6379> hdel hash-key sub-key2
16 (integer) 0
17 127.0.0.1:6379> hgetall sub-key1
18 (empty list or set)
19 127.0.0.1:6379> hget hash-key sub-key1
20 "value1"
21 127.0.0.1:6379> hgetall hash-key
22 1) "sub-key1"
23 2) "value1"
24 127.0.0.1:6379>
View Code

ZSET

1 Last login: Mon Jul 26 19:32:40 on ttys001
2 Taojie:~ apple$ redis-cli
3 127.0.0.1:6379> zadd zset-key 728 member1
4 (integer) 1
5 127.0.0.1:6379> zadd zset-key 982 member0
6 (integer) 1
7 127.0.0.1:6379> zadd zset-key 982 member0
8 (integer) 0
9 127.0.0.1:6379> zrange zset-key 0 -1 withscores
10 1) "member1"
11 2) "728"
12 3) "member0"
13 4) "982"
14 127.0.0.1:6379> zrangebyscore zset-key 0 800 withscores
15 1) "member1"
16 2) "728"
17 127.0.0.1:6379> zrem zset-key member1
18 (integer) 1
19 127.0.0.1:6379> zrem zset-key member1
20 (integer) 0
21 127.0.0.1:6379> zrange zset-key 0 -1 withscores
22 1) "member0"
23 2) "982"
24 127.0.0.1:6379>
View Code

三、資料結構
字典
dictht 是一個散串列結構,使用拉鏈法解決哈希沖突,
1 /* This is our hash table structure. Every dictionary has two of this as we
2 * implement incremental rehashing, for the old to the new table. */
3 typedef struct dictht {
4 dictEntry **table;
5 unsigned long size;
6 unsigned long sizemask;
7 unsigned long used;
8 } dictht;
View Code
1 typedef struct dictEntry {
2 void *key;
3 union {
4 void *val;
5 uint64_t u64;
6 int64_t s64;
7 double d;
8 } v;
9 struct dictEntry *next;
10 } dictEntry;
View Code
Redis 的字典 dict 中包含兩個哈希表 dictht,這是為了方便進行 rehash 操作,在擴容時,將其中一個 dictht 上的鍵值對 rehash 到另一個 dictht 上面,完成之后釋放空間并交換兩個 dictht 的角色,
1 typedef struct dict {
2 dictType *type;
3 void *privdata;
4 dictht ht[2];
5 long rehashidx; /* rehashing not in progress if rehashidx == -1 */
6 unsigned long iterators; /* number of iterators currently running */
7 } dict;
View Code
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 去執行,
1 /* Performs N steps of incremental rehashing. Returns 1 if there are still
2 * keys to move from the old to the new hash table, otherwise 0 is returned.
3 *
4 * Note that a rehashing step consists in moving a bucket (that may have more
5 * than one key as we use chaining) from the old to the new hash table, however
6 * since part of the hash table may be composed of empty spaces, it is not
7 * guaranteed that this function will rehash even a single bucket, since it
8 * will visit at max N*10 empty buckets in total, otherwise the amount of
9 * work it does would be unbound and the function may block for a long time. */
10 int dictRehash(dict *d, int n) {
11 int empty_visits = n * 10; /* Max number of empty buckets to visit. */
12 if (!dictIsRehashing(d)) return 0;
13
14 while (n-- && d->ht[0].used != 0) {
15 dictEntry *de, *nextde;
16
17 /* Note that rehashidx can't overflow as we are sure there are more
18 * elements because ht[0].used != 0 */
19 assert(d->ht[0].size > (unsigned long) d->rehashidx);
20 while (d->ht[0].table[d->rehashidx] == NULL) {
21 d->rehashidx++;
22 if (--empty_visits == 0) return 1;
23 }
24 de = d->ht[0].table[d->rehashidx];
25 /* Move all the keys in this bucket from the old to the new hash HT */
26 while (de) {
27 uint64_t h;
28
29 nextde = de->next;
30 /* Get the index in the new hash table */
31 h = dictHashKey(d, de->key) & d->ht[1].sizemask;
32 de->next = d->ht[1].table[h];
33 d->ht[1].table[h] = de;
34 d->ht[0].used--;
35 d->ht[1].used++;
36 de = nextde;
37 }
38 d->ht[0].table[d->rehashidx] = NULL;
39 d->rehashidx++;
40 }
41
42 /* Check if we already rehashed the whole table... */
43 if (d->ht[0].used == 0) {
44 zfree(d->ht[0].table);
45 d->ht[0] = d->ht[1];
46 _dictReset(&d->ht[1]);
47 d->rehashidx = -1;
48 return 0;
49 }
50
51 /* More to rehash... */
52 return 1;
53 }
View Code
跳躍表
是有序集合的底層實作之一,
跳躍表是基于多指標有序鏈表實作的,可以看成多個有序鏈表,

在查找時,從上層指標開始查找,找到對應的區間之后再到下一層去查找,下圖演示了查找 22 的程序,

與紅黑樹等平衡樹相比,跳躍表具有以下優點:
- 插入速度非常快速,因為不需要進行旋轉等操作來維護平衡性;
- 更容易實作;
- 支持無鎖操作,
四、使用場景
計數器
可以對 String 進行自增自減運算,從而實作計數器功能,
Redis 這種記憶體型資料庫的讀寫性能非常高,很適合存盤頻繁讀寫的計數量,
快取
將熱點資料放到記憶體中,設定記憶體的最大使用量以及淘汰策略來保證快取的命中率,
查找表
例如 DNS 記錄就很適合使用 Redis 進行存盤,
查找表和快取類似,也是利用了 Redis 快速的查找特性,但是查找表的內容不能失效,而快取的內容可以失效,因為快取不作為可靠的資料來源,
訊息佇列
List 是一個雙向鏈表,可以通過 lpush 和 rpop 寫入和讀取訊息
不過最好使用 Kafka、RabbitMQ 等訊息中間件,
會話快取
可以使用 Redis 來統一存盤多臺應用服務器的會話資訊,
當應用服務器不再存盤用戶的會話資訊,也就不再具有狀態,一個用戶可以請求任意一個應用服務器,從而更容易實作高可用性以及可伸縮性,
分布式鎖實作
在分布式場景下,無法使用單機環境下的鎖來對多個節點上的行程進行同步,
可以使用 Redis 自帶的 SETNX 命令實作分布式鎖,除此之外,還可以使用官方提供的 RedLock 分布式鎖實作,
其它
Set 可以實作交集、并集等操作,從而實作共同好友等功能,
ZSet 可以實作有序性操作,從而實作排行榜等功能,
五、Redis 與 Memcached
兩者都是非關系型記憶體鍵值資料庫,主要有以下不同:
資料型別
Memcached 僅支持字串型別,而 Redis 支持五種不同的資料型別,可以更靈活地解決問題,
資料持久化
Redis 支持兩種持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化,
分布式
Memcached 不支持分布式,只能通過在客戶端使用一致性哈希來實作分布式存盤,這種方式在存盤和查詢時都需要先在客戶端計算一次資料所在的節點,
Redis Cluster 實作了分布式的支持,
記憶體管理機制
-
在 Redis 中,并不是所有資料都一直存盤在記憶體中,可以將一些很久沒用的 value 交換到磁盤,而 Memcached 的資料則會一直在記憶體中,
-
Memcached 將記憶體分割成特定長度的塊來存盤資料,以完全解決記憶體碎片的問題,但是這種方式會使得記憶體的利用率不高,例如塊的大小為 128 bytes,只存盤 100 bytes 的資料,那么剩下的 28 bytes 就浪費掉了,
六、鍵的過期時間
Redis 可以為每個鍵設定過期時間,當鍵過期時,會自動洗掉該鍵,
對于散串列這種容器,只能為整個鍵設定過期時間(整個散串列),而不能為鍵里面的單個元素設定過期時間,
七、資料淘汰策略
可以設定記憶體最大使用量,當記憶體使用量超出時,會施行資料淘汰策略,
Redis 具體有 6 種淘汰策略:
| 策略 | 描述 |
|---|---|
| volatile-lru | 從已設定過期時間的資料集中挑選最近最少使用的資料淘汰 |
| volatile-ttl | 從已設定過期時間的資料集中挑選將要過期的資料淘汰 |
| volatile-random | 從已設定過期時間的資料集中任意選擇資料淘汰 |
| allkeys-lru | 從所有資料集中挑選最近最少使用的資料淘汰 |
| allkeys-random | 從所有資料集中任意選擇資料進行淘汰 |
| noeviction | 禁止驅逐資料 |
作為記憶體資料庫,出于對性能和記憶體消耗的考慮,Redis 的淘汰演算法實際實作上并非針對所有 key,而是抽樣一小部分并且從中選出被淘汰的 key,
使用 Redis 快取資料時,為了提高快取命中率,需要保證快取資料都是熱點資料,可以將記憶體最大使用量設定為熱點資料占用的記憶體量,然后啟用 allkeys-lru 淘汰策略,將最近最少使用的資料淘汰,
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通過統計訪問頻率,將訪問頻率最少的鍵值對淘汰,
八、持久化
Redis 是記憶體型資料庫,為了保證資料在斷電后不會丟失,需要將記憶體中的資料持久化到硬碟上,
RDB 持久化
將某個時間點的所有資料都存放到硬碟上,
可以將快照復制到其它服務器從而創建具有相同資料的服務器副本,
如果系統發生故障,將會丟失最后一次創建快照之后的資料,
如果資料量很大,保存快照的時間會很長,
AOF 持久化
將寫命令添加到 AOF 檔案(Append Only File)的末尾,
使用 AOF 持久化需要設定同步選項,從而確保寫命令同步到磁盤檔案上的時機,這是因為對檔案進行寫入并不會馬上將內容同步到磁盤上,而是先存盤到緩沖區,然后由作業系統決定什么時候同步到磁盤,有以下同步選項:
| 選項 | 同步頻率 |
|---|---|
| always | 每個寫命令都同步 |
| everysec | 每秒同步一次 |
| no | 讓作業系統來決定何時同步 |
- always 選項會嚴重減低服務器的性能;
- everysec 選項比較合適,可以保證系統崩潰時只會丟失一秒左右的資料,并且 Redis 每秒執行一次同步對服務器性能幾乎沒有任何影響;
- no 選項并不能給服務器性能帶來多大的提升,而且也會增加系統崩潰時資料丟失的數量,
隨著服務器寫請求的增多,AOF 檔案會越來越大,Redis 提供了一種將 AOF 重寫的特性,能夠去除 AOF 檔案中的冗余寫命令,
九、事務
一個事務包含了多個命令,服務器在執行事務期間,不會改去執行其它客戶端的命令請求,
事務中的多個命令被一次性發送給服務器,而不是一條一條發送,這種方式被稱為流水線,它可以減少客戶端與服務器之間的網路通信次數從而提升性能,
Redis 最簡單的事務實作方式是使用 MULTI 和 EXEC 命令將事務操作包圍起來,
十、事件
Redis 服務器是一個事件驅動程式,
檔案事件
服務器通過套接字與客戶端或者其它服務器進行通信,檔案事件就是對套接字操作的抽象,
Redis 基于 Reactor 模式開發了自己的網路事件處理器,使用 I/O 多路復用程式來同時監聽多個套接字,并將到達的事件傳送給檔案事件分派器,分派器會根據套接字產生的事件型別呼叫相應的事件處理器,

時間事件
服務器有一些操作需要在給定的時間點執行,時間事件是對這類定時操作的抽象,
時間事件又分為:
- 定時事件:是讓一段程式在指定的時間之內執行一次;
- 周期性事件:是讓一段程式每隔指定時間就執行一次,
Redis 將所有時間事件都放在一個無序鏈表中,通過遍歷整個鏈表查找出已到達的時間事件,并呼叫相應的事件處理器,
事件的調度與執行
服務器需要不斷監聽檔案事件的套接字才能得到待處理的檔案事件,但是不能一直監聽,否則時間事件無法在規定的時間內執行,因此監聽時間應該根據距離現在最近的時間事件來決定,
事件調度與執行由 aeProcessEvents 函式負責,偽代碼如下:
1 def aeProcessEvents():
2 # 獲取到達時間離當前時間最接近的時間事件
3 time_event = aeSearchNearestTimer()
4 # 計算最接近的時間事件距離到達還有多少毫秒
5 remaind_ms = time_event.when - unix_ts_now()
6 # 如果事件已到達,那么 remaind_ms 的值可能為負數,將它設為 0
7 if remaind_ms < 0:
8 remaind_ms = 0
9 # 根據 remaind_ms 的值,創建 timeval
10 timeval = create_timeval_with_ms(remaind_ms)
11 # 阻塞并等待檔案事件產生,最大阻塞時間由傳入的 timeval 決定
12 aeApiPoll(timeval)
13 # 處理所有已產生的檔案事件
14 procesFileEvents()
15 # 處理所有已到達的時間事件
16 processTimeEvents()
View Code
將 aeProcessEvents 函式置于一個回圈里面,加上初始化和清理函式,就構成了 Redis 服務器的主函式,偽代碼如下:
1 def main():
2 # 初始化服務器
3 init_server()
4 # 一直處理事件,直到服務器關閉為止
5 while server_is_not_shutdown():
6 aeProcessEvents()
7 # 服務器關閉,執行清理操作
8 clean_server()
View Code
從事件處理的角度來看,服務器運行流程如下:

十一、復制
通過使用 slaveof host port 命令來讓一個服務器成為另一個服務器的從服務器,
一個從服務器只能有一個主服務器,并且不支持主主復制,
連接程序
-
主服務器創建快照檔案,發送給從服務器,并在發送期間使用緩沖區記錄執行的寫命令,快照檔案發送完畢之后,開始向從服務器發送存盤在緩沖區中的寫命令;
-
從服務器丟棄所有舊資料,載入主服務器發來的快照檔案,之后從服務器開始接受主服務器發來的寫命令;
-
主服務器每執行一次寫命令,就向從服務器發送相同的寫命令,
主從鏈
隨著負載不斷上升,主服務器可能無法很快地更新所有從服務器,或者重新連接和重新同步從服務器將導致系統超載,為了解決這個問題,可以創建一個中間層來分擔主服務器的復制作業,中間層的服務器是最上層服務器的從服務器,又是最下層服務器的主服務器,

十二、Sentinel
Sentinel(哨兵)可以監聽集群中的服務器,并在主服務器進入下線狀態時,自動從從服務器中選舉出新的主服務器,
十三、分片
分片是將資料劃分為多個部分的方法,可以將資料存盤到多臺機器里面,這種方法在解決某些問題時可以獲得線性級別的性能提升,
假設有 4 個 Redis 實體 R0,R1,R2,R3,還有很多表示用戶的鍵 user:1,user:2,... ,有不同的方式來選擇一個指定的鍵存盤在哪個實體中,
- 最簡單的方式是范圍分片,例如用戶 id 從 0~1000 的存盤到實體 R0 中,用戶 id 從 1001~2000 的存盤到實體 R1 中,等等,但是這樣需要維護一張映射范圍表,維護操作代價很高,
- 還有一種方式是哈希分片,使用 CRC32 哈希函式將鍵轉換為一個數字,再對實體數量求模就能知道應該存盤的實體,
根據執行分片的位置,可以分為三種分片方式:
- 客戶端分片:客戶端使用一致性哈希等演算法決定鍵應當分布到哪個節點,
- 代理分片:將客戶端請求發送到代理上,由代理轉發請求到正確的節點上,
- 服務器分片:Redis Cluster,
十四、一個簡單的論壇系統分析
該論壇系統功能如下:
- 可以發布文章;
- 可以對文章進行點贊;
- 在首頁可以按文章的發布時間或者文章的點贊數進行排序顯示,
文章資訊
文章包括標題、作者、贊數等資訊,在關系型資料庫中很容易構建一張表來存盤這些資訊,在 Redis 中可以使用 HASH 來存盤每種資訊以及其對應的值的映射,
Redis 沒有關系型資料庫中的表這一概念來將同種型別的資料存放在一起,而是使用命名空間的方式來實作這一功能,鍵名的前面部分存盤命名空間,后面部分的內容存盤 ID,通常使用 : 來進行分隔,例如下面的 HASH 的鍵名為 article:92617,其中 article 為命名空間,ID 為 92617,

點贊功能
當有用戶為一篇文章點贊時,除了要對該文章的 votes 欄位進行加 1 操作,還必須記錄該用戶已經對該文章進行了點贊,防止用戶點贊次數超過 1,可以建立文章的已投票用戶集合來進行記錄,
為了節約記憶體,規定一篇文章發布滿一周之后,就不能再對它進行投票,而文章的已投票集合也會被洗掉,可以為文章的已投票集合設定一個一周的過期時間就能實作這個規定,

對文章進行排序
為了按發布時間和點贊數進行排序,可以建立一個文章發布時間的有序集合和一個文章點贊數的有序集合,(下圖中的 score 就是這里所說的點贊數;下面所示的有序集合分值并不直接是時間和點贊數,而是根據時間和點贊數間接計算出來的)

面試 Redis 三大快取參考鏈接:https://www.cnblogs.com/taojietaoge/p/13672295.html
春宵一刻值千金
花有清香月有陰
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/296774.html
標籤:其它

