1. 概述
1.1. Redis 簡介
Redis 是速度非常快的非關係型(NoSQL)內存鍵值數據庫,可以存儲鍵和五種不同類型的值之間的映射。
鍵的類型只能爲字符串,值支持的五種類型數據類型爲:字符串、列表、集合、有序集合、散列表。
Redis 支持很多特性,例如將內存中的數據持久化到硬盤中,使用複製來擴展讀性能,使用分片來擴展寫性能。
1.2. Redis 的優勢
-
性能極高 – Redis 能讀的速度是 110000 次/s,寫的速度是 81000 次/s。
-
豐富的數據類型 - 支持字符串、列表、集合、有序集合、散列表。
-
原子 - Redis 的所有操作都是原子性的。單個操作是原子性的。多個操作也支持事務,即原子性,通過 MULTI 和 EXEC 指令包起來。
-
持久化 - Redis 支持數據的持久化。可以將內存中的數據保存在磁盤中,重啓的時候可以再次加載進行使用。
-
備份 - Redis 支持數據的備份,即 master-slave 模式的數據備份。
-
豐富的特性 - Redis 還支持發佈訂閱, 通知, key 過期等等特性。
1.3. Redis 與 Memcached
Redis 與 Memcached 因爲都可以用於緩存,所以常常被拿來做比較,二者主要有以下區別:
數據類型
-
Memcached 僅支持字符串類型;
-
而 Redis 支持五種不同種類的數據類型,使得它可以更靈活地解決問題。
數據持久化
-
Memcached 不支持持久化;
-
Redis 支持兩種持久化策略:RDB 快照和 AOF 日誌。
分佈式
-
Memcached 不支持分佈式,只能通過在客戶端使用像一致性哈希這樣的分佈式算法來實現分佈式存儲,這種方式在存儲和查詢時都需要先在客戶端計算一次數據所在的節點。
-
Redis Cluster 實現了分佈式的支持。
內存管理機制
-
Memcached 將內存分割成特定長度的塊來存儲數據,以完全解決內存碎片的問題,但是這種方式會使得內存的利用率不高,例如塊的大小爲 128 bytes,只存儲 100 bytes 的數據,那麼剩下的 28 bytes 就浪費掉了。
-
在 Redis 中,並不是所有數據都一直存儲在內存中,可以將一些很久沒用的 value 交換到磁盤。而 Memcached 的數據則會一直在內存中。
2. 數據類型
2.1. STRING
命令:
命令 | 行爲 |
GET | 獲取存儲在給定鍵中的值 |
SET | 設置存儲在給定鍵中的值 |
DEL | 刪除存儲在給定鍵中的值(這個命令可以用於所有類型) |
示例:
127.0.0.1:6379> set name jack
OK
127.0.0.1:6379> get name
"jack"
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> get name
(nil)
2.2. LIST
命令:
命令 | 行爲 |
RPUSH | 獲取存儲在給定鍵中的值 |
LRANGE | 設置存儲在給定鍵中的值 |
LINDEX | 刪除存儲在給定鍵中的值(這個命令可以用於所有類型) |
LPOP | 刪除存儲在給定鍵中的值(這個命令可以用於所有類型) |
示例:
127.0.0.1:6379> rpush list item1
(integer) 1
127.0.0.1:6379> rpush list item2
(integer) 2
127.0.0.1:6379> rpush list item3
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "item1"
2) "item2"
3) "item3"
127.0.0.1:6379> lindex list 1
"item2"
127.0.0.1:6379> lpop list
"item1"
127.0.0.1:6379> lrange list 0 -1
1) "item2"
2) "item3"
2.3. SET
命令:
命令 | 行爲 |
SADD | 添加一個或多個元素到集合裏 |
SMEMBERS | 獲取集合裏面的所有元素 |
SISMEMBER | 確定一個給定的值是一個集合的成員 |
SREM | 從集合裏刪除一個或多個元素 |
示例:
127.0.0.1:6379> sadd set item1
(integer) 1
127.0.0.1:6379> sadd set item2
(integer) 1
127.0.0.1:6379> sadd set item3
(integer) 1
127.0.0.1:6379> sadd set item3
(integer) 0
127.0.0.1:6379> smembers set
1) "item3"
2) "item2"
3) "item1"
127.0.0.1:6379> sismember set item2
(integer) 1
127.0.0.1:6379> sismember set item6
(integer) 0
127.0.0.1:6379> srem set item2
(integer) 1
127.0.0.1:6379> srem set item2
(integer) 0
127.0.0.1:6379> smembers set
1) "item3"
2) "item1"
2.4. HASH
命令:
命令 | 行爲 |
HSET | 設置 hash 裏面一個字段的值 |
HGET | 獲取 hash 中域的值 |
HGETALL | 從 hash 中讀取全部的域和值 |
HDEL | 刪除一個或多個域 |
示例:
127.0.0.1:6379> hset myhash key1 value1
(integer) 1
127.0.0.1:6379> hset myhash key2 value2
(integer) 1
127.0.0.1:6379> hset myhash key3 value3
(integer) 1
127.0.0.1:6379> hset myhash key3 value2
(integer) 0
127.0.0.1:6379> hgetall myhash
1) "key1"
2) "value1"
3) "key2"
4) "value2"
5) "key3"
6) "value2"
127.0.0.1:6379> hdel myhash key2
(integer) 1
127.0.0.1:6379> hdel myhash key2
(integer) 0
127.0.0.1:6379> hget myhash key2
(nil)
127.0.0.1:6379> hgetall myhash
1) "key1"
2) "value1"
3) "key3"
4) "value2"
127.0.0.1:6379>
2.5. ZSET
命令:
命令 | 行爲 |
ZADD | 添加到有序 set 的一個或多個成員,或更新的分數,如果它已經存在 |
ZRANGE | 根據指定的 index 返回,返回 sorted set 的成員列表 |
ZRANGEBYSCORE | 返回有序集合中指定分數區間內的成員,分數由低到高排序。 |
ZREM | 從排序的集合中刪除一個或多個成員 |
示例:
127.0.0.1:6379> zadd zset 1 redis
(integer) 1
127.0.0.1:6379> zadd zset 2 mongodb
(integer) 1
127.0.0.1:6379> zadd zset 3 mysql
(integer) 1
127.0.0.1:6379> zadd zset 3 mysql
(integer) 0
127.0.0.1:6379> zadd zset 4 mysql
(integer) 0
127.0.0.1:6379> zrange zset 0 -1 withscores
1) "redis"
2) "1"
3) "mongodb"
4) "2"
5) "mysql"
6) "4"
127.0.0.1:6379> zrangebyscore zset 0 2 withscores
1) "redis"
2) "1"
3) "mongodb"
4) "2"
127.0.0.1:6379> zrem zset mysql
(integer) 1
127.0.0.1:6379> zrange zset 0 -1 withscores
1) "redis"
2) "1"
3) "mongodb"
4) "2"
3. Redis 使用場景
-
緩存 - 將熱點數據放到內存中,設置內存的最大使用量以及過期淘汰策略來保證緩存的命中率。
-
計數器 - Redis 這種內存數據庫能支持計數器頻繁的讀寫操作。
-
應用限流 - 限制一個網站訪問流量。
-
消息隊列 - 使用 List 數據類型,它是雙向鏈表。
-
查找表 - 使用 HASH 數據類型。
-
交集運算 - 使用 SET 類型,例如求兩個用戶的共同好友。
-
排行榜 - 使用 ZSET 數據類型。
-
分佈式 Session - 多個應用服務器的 Session 都存儲到 Redis 中來保證 Session 的一致性。
-
分佈式鎖 - 除了可以使用 SETNX 實現分佈式鎖之外,還可以使用官方提供的 RedLock 分佈式鎖實現。
4. Redis 管道
Redis 是一種基於 C/S 模型以及請求/響應協議的 TCP 服務。
Redis 支持管道技術。管道技術允許請求以異步方式發送,即舊請求的應答還未返回的情況下,允許發送新請求。這種方式可以大大提高傳輸效率。
使用管道發送命令時,服務器將被迫回覆一個隊列答覆,佔用很多內存。所以,如果你需要發送大量的命令,最好是把他們按照合理數量分批次的處理。
5. 鍵的過期時間
Redis 可以爲每個鍵設置過期時間,當鍵過期時,會自動刪除該鍵。
對於散列表這種容器,只能爲整個鍵設置過期時間(整個散列表),而不能爲鍵裏面的單個元素設置過期時間。
可以使用 EXPIRE 或 EXPIREAT 來爲 key 設置過期時間。
注意:當 EXPIRE 的時間如果設置的是負數,EXPIREAT 設置的時間戳是過期時間,將直接刪除 key。
示例:
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
redis> SET mykey "Hello World"
"OK"
redis> TTL mykey
(integer) -1
redis>
6. 內存淘汰
6.1. 內存淘汰要點
-
最大緩存 - Redis 允許通過
maxmemory
參數來設置內存最大值。 -
主鍵失效 - 作爲一種定期清理無效數據的重要機制,在 Redis 提供的諸多命令中,
EXPIRE
、EXPIREAT
、PEXPIRE
、PEXPIREAT
以及SETEX
和PSETEX
均可以用來設置一條鍵值對的失效時間。而一條鍵值對一旦被關聯了失效時間就會在到期後自動刪除(或者說變得無法訪問更爲準確)。 -
淘汰策略 - 隨着不斷的向 redis 中保存數據,當內存剩餘空間無法滿足添加的數據時,redis 內就會施行數據淘汰策略,清除一部分內容然後保證新的數據可以保存到內存中。內存淘汰機制是爲了更好的使用內存,用一定得 miss 來換取內存的利用率,保證 redis 緩存中保存的都是熱點數據。
-
非精準的 LRU - 實際上 Redis 實現的 LRU 並不是可靠的 LRU,也就是名義上我們使用 LRU 算法淘汰鍵,但是實際上被淘汰的鍵並不一定是真正的最久沒用的。
6.2. 淘汰策略
內存淘汰只是 Redis 提供的一個功能,爲了更好地實現這個功能,必須爲不同的應用場景提供不同的策略,內存淘汰策略講的是爲實現內存淘汰我們具體怎麼做,要解決的問題包括淘汰鍵空間如何選擇?在鍵空間中淘汰鍵如何選擇?
Redis 提供了下面幾種淘汰策略供用戶選擇,其中默認的策略爲 noeviction 策略:
-
`noeviction` - 當內存使用達到閾值的時候,所有引起申請內存的命令會報錯。
-
`allkeys-lru` - 在主鍵空間中,優先移除最近未使用的 key。
-
`allkeys-random` - 在主鍵空間中,隨機移除某個 key。
-
`volatile-lru` - 在設置了過期時間的鍵空間中,優先移除最近未使用的 key。
-
`volatile-random` - 在設置了過期時間的鍵空間中,隨機移除某個 key。
-
`volatile-ttl` - 在設置了過期時間的鍵空間中,具有更早過期時間的 key 優先移除。
6.3. 如何選擇淘汰策略
-
如果數據呈現冪律分佈,也就是一部分數據訪問頻率高,一部分數據訪問頻率低,則使用 allkeys-lru。
-
如果數據呈現平等分佈,也就是所有的數據訪問頻率都相同,則使用 allkeys-random。
-
volatile-lru 策略和 volatile-random 策略適合我們將一個 Redis 實例既應用於緩存和又應用於持久化存儲的時候,然而我們也可以通過使用兩個 Redis 實例來達到相同的效果。
-
將 key 設置過期時間實際上會消耗更多的內存,因此我們建議使用 allkeys-lru 策略從而更有效率的使用內存。
6.4. 內部實現
Redis 刪除失效主鍵的方法主要有兩種:
-
消極方法(passive way),在主鍵被訪問時如果發現它已經失效,那麼就刪除它。
-
主動方法(active way),週期性地從設置了失效時間的主鍵中選擇一部分失效的主鍵刪除。
-
主動刪除:當前已用內存超過 maxmemory 限定時,觸發主動清理策略,該策略由啓動參數的配置決定主鍵具體的失效時間全部都維護在 expires 這個字典表中。
7. 持久化
Redis 是內存型數據庫,爲了保證數據在斷電後不會丟失,需要將內存中的數據持久化到硬盤上。
7.1. 快照持久化
將某個時間點的所有數據都存放到硬盤上。
可以將快照複製到其它服務器從而創建具有相同數據的服務器副本。
如果系統發生故障,將會丟失最後一次創建快照之後的數據。
如果數據量很大,保存快照的時間會很長。
7.2. AOF 持久化
將寫命令添加到 AOF 文件(Append Only File)的末尾。
對硬盤的文件進行寫入時,寫入的內容首先會被存儲到緩衝區,然後由操作系統決定什麼時候將該內容同步到硬盤,用戶可以調用 file.flush() 方法請求操作系統儘快將緩衝區存儲的數據同步到硬盤。可以看出寫入文件的數據不會立即同步到硬盤上,在將寫命令添加到 AOF 文件時,要根據需求來保證何時同步到硬盤上。
有以下同步選項:
選項 | 同步頻率 |
always | 每個寫命令都同步 |
everysec | 每秒同步一次 |
no | 讓操作系統來決定何時同步 |
-
always 選項會嚴重減低服務器的性能;
-
everysec 選項比較合適,可以保證系統奔潰時只會丟失一秒左右的數據,並且 Redis 每秒執行一次同步對服務器性能幾乎沒有任何影響;
-
no 選項並不能給服務器性能帶來多大的提升,而且也會增加系統奔潰時數據丟失的數量。
隨着服務器寫請求的增多,AOF 文件會越來越大。Redis 提供了一種將 AOF 重寫的特性,能夠去除 AOF 文件中的冗餘寫命令。
8. 發佈與訂閱
訂閱者訂閱了頻道之後,發佈者向頻道發送字符串消息會被所有訂閱者接收到。
某個客戶端使用 SUBSCRIBE 訂閱一個頻道,其它客戶端可以使用 PUBLISH 向這個頻道發送消息。
發佈與訂閱模式和觀察者模式有以下不同:
-
觀察者模式中,觀察者和主題都知道對方的存在;而在發佈與訂閱模式中,發佈者與訂閱者不知道對方的存在,它們之間通過頻道進行通信。
-
觀察者模式是同步的,當事件觸發時,主題會去調用觀察者的方法;而發佈與訂閱模式是異步的;
9. 事務
MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事務相關的命令。
事務可以一次執行多個命令, 並且有以下兩個重要的保證:
-
事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。
-
事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。
9.1. EXEC
EXEC 命令負責觸發並執行事務中的所有命令。
-
如果客戶端在使用
MULTI
開啓了一個事務之後,卻因爲斷線而沒有成功執行EXEC
,那麼事務中的所有命令都不會被執行。 -
另一方面,如果客戶端成功在開啓事務之後執行
EXEC
,那麼事務中的所有命令都會被執行。
9.2. MULTI
MULTI 命令用於開啓一個事務,它總是返回 OK 。
MULTI 執行之後, 客戶端可以繼續向服務器發送任意多條命令, 這些命令不會立即被執行, 而是被放到一個隊列中, 當 EXEC 命令被調用時, 所有隊列中的命令纔會被執行。
以下是一個事務例子, 它原子地增加了 foo 和 bar 兩個鍵的值:
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
9.3. DISCARD
當執行 DISCARD 命令時, 事務會被放棄, 事務隊列會被清空, 並且客戶端會從事務狀態中退出。
示例:
> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"
9.4. WATCH
WATCH 命令可以爲 Redis 事務提供 check-and-set (CAS)行爲。
被 WATCH 的鍵會被監視,並會發覺這些鍵是否被改動過了。如果有至少一個被監視的鍵在 EXEC 執行之前被修改了, 那麼整個事務都會被取消, EXEC 返回 nil-reply 來表示事務已經失敗。
WATCH mykey
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
使用上面的代碼, 如果在 WATCH 執行之後, EXEC 執行之前, 有其他客戶端修改了 mykey 的值, 那麼當前客戶端的事務就會失敗。程序需要做的, 就是不斷重試這個操作, 直到沒有發生碰撞爲止。
這種形式的鎖被稱作樂觀鎖, 它是一種非常強大的鎖機制。並且因爲大多數情況下, 不同的客戶端會訪問不同的鍵, 碰撞的情況一般都很少, 所以通常並不需要進行重試。
WATCH 使得 EXEC 命令需要有條件地執行:事務只能在所有被監視鍵都沒有被修改的前提下執行,如果這個前提不能滿足的話,事務就不會被執行。
WATCH 命令可以被調用多次。對鍵的監視從 WATCH 執行之後開始生效,直到調用 EXEC 爲止。
用戶還可以在單個 WATCH 命令中監視任意多個鍵,例如:
redis> WATCH key1 key2 key3
OK
取消 WATCH 的場景
當 EXEC 被調用時, 不管事務是否成功執行, 對所有鍵的監視都會被取消。
另外, 當客戶端斷開連接時, 該客戶端對鍵的監視也會被取消。
使用無參數的 UNWATCH 命令可以手動取消對所有鍵的監視。對於一些需要改動多個鍵的事務, 有時候程序需要同時對多個鍵進行加鎖, 然後檢查這些鍵的當前值是否符合程序的要求。當值達不到要求時, 就可以使用 UNWATCH 命令來取消目前對鍵的監視, 中途放棄這個事務, 並等待事務的下次嘗試。
使用 WATCH 創建原子操作
WATCH 可以用於創建 Redis 沒有內置的原子操作。
舉個例子,以下代碼實現了原創的 ZPOP 命令,它可以原子地彈出有序集合中分值(score)最小的元素:
WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset elementEXEC
9.5. Redis 不支持回滾
Redis 不支持回滾的理由:
-
Redis 命令只會因爲錯誤的語法而失敗,或是命令用在了錯誤類型的鍵上面。
-
因爲不需要對回滾進行支持,所以 Redis 的內部可以保持簡單且快速。
10. 事件
Redis 服務器是一個事件驅動程序。
Redis 服務器需要處理兩類事件:
-
文件事件
-
時間事件
10.1. 文件事件
服務器通過套接字與客戶端或者其它服務器進行通信,文件事件就是對套接字操作的抽象。
Redis 基於 Reactor 模式開發了自己的網絡時間處理器,使用 I/O 多路複用程序來同時監聽多個套接字,並將到達的時間傳送給文件事件分派器,分派器會根據套接字產生的事件類型調用響應的時間處理器。
10.2. 時間事件
服務器有一些操作需要在給定的時間點執行,時間事件是對這類定時操作的抽象。
時間事件又分爲:
-
定時事件:是讓一段程序在指定的時間之內執行一次;
-
週期性事件:是讓一段程序每隔指定時間就執行一次。
Redis 將所有時間事件都放在一個無序鏈表中,通過遍歷整個鏈表查找出已到達的時間事件,並調用響應的事件處理器。
10.3. 事件的調度與執行
服務器需要不斷監聽文件事件的套接字才能得到待處理的文件事件,但是不能一直監聽,否則時間事件無法在規定的時間內執行,因此監聽時間應該根據距離現在最近的時間事件來決定。
事件調度與執行由 aeProcessEvents 函數負責,僞代碼如下:
def aeProcessEvents():
## 獲取到達時間離當前時間最接近的時間事件
time_event = aeSearchNearestTimer()
## 計算最接近的時間事件距離到達還有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
## 如果事件已到達,那麼 remaind_ms 的值可能爲負數,將它設爲 0
if remaind_ms < 0:
remaind_ms = 0
## 根據 remaind_ms 的值,創建 timeval
timeval = create_timeval_with_ms(remaind_ms)
## 阻塞並等待文件事件產生,最大阻塞時間由傳入的 timeval 決定
aeApiPoll(timeval)
## 處理所有已產生的文件事件
procesFileEvents()
## 處理所有已到達的時間事件
processTimeEvents()
將 aeProcessEvents 函數置於一個循環裏面,加上初始化和清理函數,就構成了 Redis 服務器的主函數,僞代碼如下:
def main():
## 初始化服務器
init_server()
## 一直處理事件,直到服務器關閉爲止
while server_is_not_shutdown():
aeProcessEvents()
## 服務器關閉,執行清理操作
clean_server()
從事件處理的角度來看,服務器運行流程如下:
11. 集羣
11.1. 複製
通過使用 slaveof host port 命令來讓一個服務器成爲另一個服務器的從服務器。
一個從服務器只能有一個主服務器,並且不支持主主複製。
12.1. 連接過程
-
主服務器創建快照文件,發送給從服務器,並在發送期間使用緩衝區記錄執行的寫命令。快照文件發送完畢之後,開始向從服務器發送存儲在緩衝區中的寫命令;
-
從服務器丟棄所有舊數據,載入主服務器發來的快照文件,之後從服務器開始接受主服務器發來的寫命令;
-
主服務器每執行一次寫命令,就向從服務器發送相同的寫命令。
12.2. 主從鏈
隨着負載不斷上升,主服務器可能無法很快地更新所有從服務器,或者重新連接和重新同步從服務器將導致系統超載。爲了解決這個問題,可以創建一箇中間層來分擔主服務器的複製工作。中間層的服務器是最上層服務器的從服務器,又是最下層服務器的主服務器。
11.2. 哨兵
Sentinel(哨兵)可以監聽主服務器,並在主服務器進入下線狀態時,自動從從服務器中選舉出新的主服務器。
11.3. 分片
分片是將數據劃分爲多個部分的方法,可以將數據存儲到多臺機器裏面,也可以從多臺機器裏面獲取數據,這種方法在解決某些問題時可以獲得線性級別的性能提升。
假設有 4 個 Reids 實例 R0,R1,R2,R3,還有很多表示用戶的鍵 user:1,user:2,… 等等,有不同的方式來選擇一個指定的鍵存儲在哪個實例中。最簡單的方式是範圍分片,例如用戶 id 從 0~1000 的存儲到實例 R0 中,用戶 id 從 1001~2000 的存儲到實例 R1 中,等等。但是這樣需要維護一張映射範圍表,維護操作代價很高。還有一種方式是哈希分片,使用 CRC32 哈希函數將鍵轉換爲一個數字,再對實例數量求模就能知道應該存儲的實例。
主要有三種分片方式:
-
客戶端分片:客戶端使用一致性哈希等算法決定鍵應當分佈到哪個節點。
-
代理分片:將客戶端請求發送到代理上,由代理轉發請求到正確的節點上。
-
服務器分片:Redis Cluster。