Redis深刻系列-0x014:Redis數據類型和概念介紹(上)

0x000 概述

Redis不是一個簡單鍵值對存儲器,而是一個數據結構服務,它支持不一樣類型的值。這意味着傳統的鍵值對存儲器將字符串鍵和字符串值關聯起來,在Redis中,值的類型不只僅侷限於字符串,還能夠是更加複雜的數據結構,下面是Redis支持的數據結構,將會在各個章節接觸到:redis

  • 字節安全的字符串
  • 列表:根據插入順序排序的字符串集合元素,是最基本的鏈表
  • 集合:惟一的無序的字符串元素
  • 有序集合:和集合很想可是每一個字符串元素都關聯着一個浮點型數字做爲值,叫作分數。他老是按照分數排序,因此它不想集合那樣,獲取一個範圍以內的元素。
  • 哈希:是一個由鍵值對關聯起來的map,鍵和值都是字符串。對於RubyPython很是的友好。
  • 比特數組:使用特殊的命令能夠向處理比特數組同樣處理字符串,你能夠設置或者清除獨立的比特,將全部的比特設置爲1,找到第一個比特等等等
  • HyperLogLogs:不解釋。

知道這些數據類型和怎樣使用對於解決命令索引給出的問題並不老是微不足道的。知道這些數據類型和怎樣使用對於解決命令索引給出的問題是很重要的,因此這個文檔將做爲了解Redis數據類型和他們基本模式的一個入門課程。數據庫

對於全部的案例咱們將使用redis-cli工具,這是一個很簡單可是很便利的命令行工具,用來和Redis服務端作交互。數組

0x001 Rediskey

Rediskey是比特安全的,這意味着你可使用任何的二進制序列做爲key,從像foo的字符串到一個JPEG文件的內容。甚至空字符串也是能夠的。緩存

關於key有一些其餘的規則:安全

  • 很是長的key是不推薦的。一個1024 bytes是一個很是壞的注意,不只僅是由於內存浪費,更是由於在數據集中搜索對比的時候須要耗費更多的成本。當要處理的是匹配一個很是大的值,從內存和帶寬的角度來看,使用這個值的hash值是更好的辦法(好比使用SHA1)。
  • 特別短的key一般也是不推薦的。在寫像u100flw這樣的鍵的時候,有一個小小的要點,咱們能夠用user:1000:followers代替。可讀性更好,對於key對象和value對象增長的空間佔用與此相比來講卻是次要的。當短的key能夠很明顯減小空間佔用的時候,你的工做就是找到正確的平衡
  • 嘗試去固定一個密室。好比object-type:id是一個好主意,-.一般用於多個字符的域,就像comment:1234:reply.to,或者comment:1234:reply-to
  • 最大的key容許512MB

0x002 Redis字符串

Redi字符串類型是Rediskey能夠關聯的的最簡單的數據類型。這是Mmcached惟一的數據類型,因此對於Redis的使用新手來講,這是很是天然的。網絡

由於Rediskey是字符串,當咱們使用字符串類型做爲值的時候,咱們是將一個字符串映射到另外一個字符串。字符串類型在不少場景中是很是有用的,好比緩存HTML片斷或者頁面。數據結構

接下來使用redis-cli使用一下字符串類型(在這個文章中全部的示例都經過使用redis-cli):app

> set mykey somevalue
OK
> get mykey
"somevalue"

正如你看到的,使用SETGET命令能夠設置和獲取一個字符串值。值得注意的是SET將會覆蓋key已經存在的值,即便這個key關聯了一個不是字符串的值。因此SET表現爲一個任務。ide

值能夠是任意類型的字符串(包括二進制數據),好比你能夠存儲jpeg圖片。一個值不能超過512MB工具

SET命令有一些有趣的選項,做爲而外的參數。好比。我可讓SETkey已經存在的時候失敗,或者相反,只有在key存在的時候才成功:

> set mykey newval nx
(nil)
> set mykey newval xx
OK

即便字符串是Redis最基本的值,依舊有不少有趣的操做可使用。好比,原子增加:

> set counter 100
OK
> incr counter
(integer) 101
> incr counter
(integer) 102
> incrby counter 50
(integer) 152

INCR命令將字符串轉化爲integer,自增1,而後保存成新的值,還有其餘相似的命令,好比INCRBYDECRDECRBY。在內部他們實際上是同樣的命令,只是執行的時候有一點小差異而已。

INCR是原子的意味着什麼?這意味着即便多個客戶端發送INCR獲取同一個key,將不會進入競爭狀態,例如,客戶端1獲取到10,同時客戶端2也獲取到10是不可能的,所有獲取的都是11,而且將11保存成新的值。最懂的值將會是12讀取-自增-設置三個操做將在其餘客戶端還沒執行命令的時候同時完成。

有不少的命令能夠操做字符串。好比GETSET命令給一個key設置一個新的個值,同時返回舊的值做爲結果。好比,你的系統在你的網站有一個新的訪客到來的時候,使用INCR自增一個Rediskey,你可使用這個命令。你可能須要每小時收集全部的信息,甚至不錯過每一次增加,你能夠GETSET一個key,將它的值設置爲0的同時獲取新的值。

使用一個命令同時設置或者獲取多個key的能力是下降延遲的好方法。MSETMGET命令能夠作到:

> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"

MGET使用的時候,Redis將會返回一個值。

0x003 修改和查詢key空間

有一些命令在部分類型中並無定義,可是和key空間交互的時候是很是有用的,因此,可使用在任意類型的key之上。

好比,當DEL命令刪了吃了一個key和他所關聯的值的時候,EXISTS命令返回1或者0去標記一個key是否存在在數據庫,無論這個key關聯的值是什麼類型。

> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0

從例子中能夠看出,DEL命令返回1或者0取決與key是否被移除了(存在,或者沒有這個名字的key)。
From the examples you can also see how DEL itself returns 1 or 0 depending on whether the key was removed (it existed) or not (there was no such key with that name).
有很過key空間相關的命令,上面的兩個命令和TYPE命令是最主要的,TYPE命令返回的是存儲在這個key中的類型。

> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none

0x004 Redis期限:key的生存時間

在繼續瞭解更多複雜的數據類型以前,咱們須要先討論另外一個無視值類型的特性,咱們稱之爲Redis生存時間。簡單來講你能夠爲一個key設置一個過時時間,這個就是key能夠存在的時間。當能夠存在的時間過了,這個key就會自動銷燬,就像用戶使用DEL命令刪除了這個key。
關於Redis期限的一些簡單信息:

  • 他們可使用秒或者微妙做爲單位
  • 最小的單位是1微妙
  • 關於期限的信息是複製並持久化到磁盤的,當你的Redis服務端中止的時候,時間也會過去(這意味着Redis將會保存一個key的過時日期)。

設置一個過時時間是很簡單的

> set key some-value
OK
> expire key 5
(integer) 1
> get key (immediately)
"some-value"
> get key (after some time)
(nil)

在兩次相隔5s的GET調用中,key徹底消失了。在上面的例子中,咱們用EXPIRE去設置過時時間(固然也能夠用來給一個已經存在過時時間的key設置一個不一樣的過時時間,好比PERSIST能夠用來移除過時時間,使這個key永久持久化)。固然咱們也可使用其餘Redis命令建立一個有過時時間的key。好比,使用SET命令的選項:

> set key 100 ex 10
OK
> ttl key
(integer) 9

的絎棉這個栗子設置了一個值爲100,過時時間爲10秒的key,接下來的TTL命令用來檢查這個key剩下的生存時間。

爲了用毫秒設置和檢查生存時間,可使用PEXPIREPTTL命令,和完整的SET命令選項。

Redis List

爲了解釋List這種數據類型,最好先來點理論知識做爲開胃菜,其實術語List在信息技術領域的使用是常常是不恰單的。好比Python Lists並不像名字所體現的(Linked Lists),更像Arrays(實際上相同的數據類型在Ruby中稱爲Array)。

從通常的觀點看,一個List只是一個由一系列有序元素組成的列表:10,20,1,2,3。可是使用Array實現的List和用Linked List實現的List在特性上有很大的不一樣。
Redis List是經過Linked List實現的。這意味着即便你有百萬個元素在列表內,添加一個元素的操做到頭部或者尾部的操做的時間是一個常量。使用LPUSH命令添加一個新元素到一個有10個元素的列表的頭部所耗費的時間是和添加一個元素到一個有10000000萬元素的列表的頭部是同樣的。

不利的一面是什麼呢?使用Arrays實現的List經過索引訪問一個元素是很是迅速的(常量時間),然而用Linked List實現的則不會這麼快(這個操做須要的時間是和要訪問的索引成正比的)。

Redis List使用Linked List實現是由於對於數據庫系統來講,它須要可以經過很是快的方式添加元素到一個很是長的列表。接下來你將看到一個很是強的優點,那就是Redis Lists能夠採起常量長度在常量時間內。

當須要很是快的訪問一個巨大聚合元素的其中一個數據時候,有另外一種數據結構可供選擇,那就是Sorted SetsSorted Set將在下面的章節涉及。

Redis Lists使用第一步

LPUSH命令添加一個新的元素到一個列表的左邊(頭部),RPUSH命令添加一個新的元素到一個列表的右邊(尾部)。LRAGE命令從列表中提取元素。

> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"

注意:LRAGE須要兩個索引,要返回的第一個元素的索引和最後一個元素的索引。兩個因此均可以被導航,告訴Redis從開始統計到結束:因此,-1是列表的最後一個元素,-2是列表的倒數第一個元素,以此類推。

就像你看到的RPISH添加元素到列表的右邊,LPUSH添加元素到列表的左邊。
As you can see RPUSH appended the elements on the right of the list, while the final LPUSH appended the element on the left.
兩個命令都是可變參數長度的命令,,這意味着你能夠在一次執行中自由的推入多個元素到一個列表:

> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "foo bar"

定義在Redis Lists中的一個重要操做是pop的能力。彈出元素是從列表獲取元素而且淘汰元素的操做。你能夠從左邊或則右邊彈出元素,就像你能夠從列表兩邊推入元素同樣:

> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"

咱們推入了三個元素而且彈出了三個元素,因此執行完這一系列命令,這個列表最終變成空的,而且將再也不有數據彈出。若是咱們依舊嘗試彈出其餘元素,咱們將會獲得以下結果:

> rpop mylist
(nil)
Redis returned a NULL value to signal that there are no elements in the list.

Redis Lists的應用場景

Lists對一系列任務都頗有幫助,下面是兩個典型應用場景:

  • 在社交網絡中記住用戶最新更新的文章
  • 進程間交流,使用生產-消費模式,生產者推入元素到列表中,消費者消費這些元素,並執行動做。Redis有特殊的列表命令去保證這種用戶場景更加可靠和有效。

好比,Ruby的庫resquesidekiq在底層使用Redis Lists去實現後臺任務。
流行的社交網絡Twitter將最新的Twitter用戶文章推入Redis Lists

爲了一步一步歸納一個普通的用戶場景,想一想你的主頁顯示了發佈在一個照片分享社交網絡的最新的照片,你想要很快的訪問。

每次一個用戶發佈一張新的照片,咱們使用LPUSH將照片的ID放入一個列表。當用戶訪問主頁的時候,咱們使用LRANGE 0 9去獲取最新的10張照片。

有限List

在不少應用場景下,咱們只是想使用列表去存儲最新的項目,好比:社交網絡更新,日誌,諸如此類。
Redis容許咱們像使用有限集合同樣使用列表,只記住最新的N條數據並使用LTRIM拋棄掉最舊的數據。

LTRIMLRANGE很像,可是它不是現實指定的元素範圍,而是設置指定的方位爲新的列表值。全部不在這個範圍以內的元素將被移除:
An example will make it more clear:

> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"

TRIM命令告訴Redis只獲取列中中索引0到2的元素,其餘不在這個範圍內的元素所有拋棄。這讓一個簡單可是有用的模式獲得實現:向列表推入數據操做+修剪操做一塊兒,實現了添加一個新元素並拋棄超出範圍的元素:

LPUSH mylist <some element>
LTRIM mylist 0 999

上面的命令結合起來實現了添加一個新的元素到列表並獲取列表前1000條最新的元素。LRANGE命令讓你能夠獲取到定模的元素而且不準要記住每個舊的數據

注意:儘管LRANGE命令技術上是一個O(N)的命令,獲取列表頭部或者尾部很小範圍的的數據依舊是一個常量時間操做。

List會阻塞的操做

列表有一個很特別的特性讓它能夠很適合用來實現隊列,通常做爲內部進程通訊系統的構建塊:阻塞操做。
想一想你的一個進程想要將一個元素推入列表,另外一個進程想要對這些元素進行某些操做。這就是一般說的生產者/消費者模式,能夠用下面的方式簡單的實現:

  • 生產者使用LPUSH向列表推入數據。
  • 消費者使用RPOP從列表消費數據。

然而,有時列表有可能時空的,沒有什麼好執行的,因此RPOP將會返回NULL,這種狀況下,消費者強制等待一些時間而後從新

  • 強制Redis和客戶端去執行無效的命令(當列表是空的的時候,針對全部的請求其實沒有作任何的工做,只是簡單的返回NULL)。
  • 添加一個延遲去執行項目,由於一個工做進程接收到NULL後會等待一些時間。讓延遲更小,咱們在能夠在兩個執行RPOP命令之間等待更少的時間,可是會引發問題1,也就是更多的無效請求。

因此Redis實現了BRPOPBLPOP命令,這個命苦可讓RPOPLPOP能夠在列表爲空的時候堵塞:他們將只在一個新的元素添加進列表的時候執行,或者當用戶指定的超時時間到了。
這是一個關於咱們可使用的BRPOP命令示例:

> brpop tasks 5
1) "tasks"
2) "do_something"

這意味着:等待列表中的元素,可是若是5s以後沒有元素就返回。
值得注意的是,你能夠設置超時時間爲0,從而讓線程永遠等待,固然你也能夠指定多個列表,而不是一個,同一時間等待多個列表,將會收到第一個收到新元素列表的通知。
一些關於BRPOP的筆記:

  • 客戶端在一種有序的方式下運行:第一個客戶端堵塞等待一個列表,它將在其餘客戶端推入元素的時候第一個被服務,並以此類推。
  • 返回值和RPOP不同:是一個包含兩個元素的數組,他包含了key的名字,由於BRPOPBLPOP能夠作到堵塞等待多個列表的元素。
  • 若是超時時間已經到了,將會返回NULL

關於列表和堵塞操做還有更多的星系你須要知道。咱們推薦你能夠閱讀下面的更多內容:

  • 使用RPOPLPUSH命令能夠構建一個更安全的隊列或者旋轉隊列。
  • BRPOPLPUSH命令是RPOPLPUSH命令堵塞的變形。
相關文章
相關標籤/搜索