【Redis】349- Redis 入門指南

  • 2019 年 10 月 4 日
  • 筆記

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  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 element  EXEC  

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. 連接過程

  1. 主伺服器創建快照文件,發送給從伺服器,並在發送期間使用緩衝區記錄執行的寫命令。快照文件發送完畢之後,開始向從伺服器發送存儲在緩衝區中的寫命令;
  2. 從伺服器丟棄所有舊數據,載入主伺服器發來的快照文件,之後從伺服器開始接受主伺服器發來的寫命令;
  3. 主伺服器每執行一次寫命令,就向從伺服器發送相同的寫命令。

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。

12. Redis Client

Redis 社區中有多種程式語言的客戶端,可以在這裡查找合適的客戶端:https://redis.io/clients

redis 官方推薦的 Java Redis Client:

  • https://github.com/xetorthio/jedis
  • https://github.com/redisson/redisson
  • https://github.com/lettuce-io/lettuce-core

13. 資料

官網

  • https://redis.io/
  • https://github.com/antirez/redis
  • http://ifeve.com/redis-sentinel/
  • http://redisdoc.com/topic/sentinel.html

書

  • 《Redis 實戰》
  • 《Redis 設計與實現》

資源匯總

  • https://github.com/JamzyWang/awesome-redis

Redis Client

  • https://docs.spring.io/spring-data/redis/docs/1.8.13.RELEASE/reference/html/
  • https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
  • https://github.com/redisson/redisson/wiki/Table-of-Content
  • https://www.jianshu.com/p/82f0d5abb002
  • https://blog.csdn.net/everlasting_188/article/details/51073505

作者:dunwu https://github.com/dunwu/db-tutorial

Tags: