【redis基礎】-redis的基本數據類型以及一些內部編碼優化

redis作爲一個內存數據庫,優化存儲、減少內存使用空間顯得尤爲重要,首先,作爲redis的使用者,我們可以對鍵值人工優化,比如對於鍵的起名,可以使用縮略詞進行標註,這樣既可以節省空間又易懂,再比如,redis提供了四個命令可以直接操作二進制位,位操作命令可以非常緊湊的存儲布爾值,當一個網站需要存儲100萬個用戶的性別的時候,我們就可以使用位操作記錄,這樣只需要佔用100KB多的空間!

同時,redis自身也作出了存儲優化,那就是內部編碼優化。首先,redis爲每種數據類型都提供了兩種編碼方式(redis一共五種數據類型),可以使用命令OBJECT ENCODING k 來查看編碼方式;

redis中,每一個鍵值都式使用一個redisObject結構體存儲(redis是使用c寫的),其結構如圖所示:

其中:

1. type:用來標明鍵值的數據類型,它有如下幾種取值:

              REDIS_STRING 0

              REDIS_LIST 1

              REDIS_SET 2

              REDIS_ZSET 3

              REDIS_HASH 4

    也就是對應這redis中的五種數據類型;

2. encoding:表示的是鍵值再redis內部的編碼方式,編碼方式一共有九中:

              REDIS_ENCODING_RAW  0   (字符串)

              REDIS_ENCODING_INT  1   (字符串)

              REDIS_ENCODING_HT  2    (散列、集合)

              REDIS_ENCODING_ZIPMAP  3   

              REDIS_ENCODING_LINKDELIST  4   (列表)

              REDIS_ENCODING_ZIPLIST  5   (散列、列表、有序集合)

              REDIS_ENCODING_INTSET  6   (集合)

              REDIS_ENCODING_SKIPLIST  7   (有序集合)

              REDIS_ENCODING_EMBSTR  8   (字符串)

    括號中的是可以採用該種編碼的數據類型。

3. ptr:一個指針,指向的是一個變量,這個變量是sdshdr類型,用來存儲字符串,這個sdshdr變量定義如下:

    len表示的是字符串的長度,free表示的是剩餘空間,buf是字符串本身的內容(redis中字符串就是存儲再這);

4. refcount:記錄了鍵值被引用的數量

下面我們就開始分析分析redis內部對數據編碼的優化策略吧!

1. 首先,是字符串類型,字符串類型是redis中最基本的數據類型,它可以存儲任何形式的字符串,包括二進制數據,它可以用來存儲圖片、用戶郵箱、JSON化的對象,其最大容量爲512MB,還有一點需要大家瞭解,那就是字符串類型是其他四種類型的基礎,可以說,其他四種數據類型就是以不同的形式組織字符串,其常用命令有:

        SET K V :賦值操作

        GET K :根據key獲取value

        APPEND K V :向value追加數據

    當存儲的是整形的數據時,可以使用:

        INCR K:value增1(i++操作)

        DECR K:value減1

        INCRBY K n:增加指定數值

       DECRBY K n:減少指定數值

    還有四個命令進行位操作:

        GETBIT K offset:獲取一個字符串指定位置的二進制位的值(超出索引返回0)

        SETBIT K offset value:設置指定位置的二進制位的值(返回的是原來的值,若位置不存在,返回0)

        BITCOUNT K [start] [end]:獲取字符串中1的個數

        BITOP operation destkey K [K1...]:對多個字符串數據進行運算(支持的運算有OR  AND  XOR  NOT)

介紹了字符串基本的操作命令,那麼我們就來看看其內部的工作原理吧,redis對字符串主要有兩種優化。

    首先,Redis中是使用上文提到的 sdshdr類型的變量存儲字符串數據,而ptr指針指向的就是這個變量,我們舉個例子,比如現在有一個字符串str,那麼如果按照embstr編碼方式,它所佔用的內存就是redisObjecy+sdshdr+strlen(str),但是,當value的內容可以用一個64位整數表示的時候,此時redis就會對字符串進行int編碼優化,它會將字符串轉換成long類型存儲再ptr指針中,此時佔用內存空間就是redisObjetc,做個測試,我現在向redis中存儲兩條數據:  name:jason   age:24,然後看看他們的編碼方式分別是什麼:

    另外,再Redis啓動的時候,它會預先建立10000個分別存儲0~99這些數字的redisObject類型變量作爲共享變量,如果你存儲的一個字符串數據再它的範圍之內,那麼它將引用共享對象而不會創建新的redisObject,也就是說,現在redis只需要存儲鍵名和一個共享對象引用就可以了,鍵值佔用的空間是0。所以,碎玉使用字符串來存儲id這種小數字是非常節省空間的。

    注意:1. 通過配置文件參數maxmemmory設置了redis的最大空間大小後,redis將不會自動創建共享變量。

                2. 例子中的embstr編碼方式是redis3.0新加入的,它與raw編碼方式極其相似,不同的是,embstr編碼方式會將redisObjetc結構體與sdshdr變量分配在連續的內存空間,這樣做的好處是:由於分配的內存塊是連續的,使得不論是內存的分配還是釋放,所需要的操作都由原來的兩次減少到一次,同時,連續的 內存可以讓操作系統的緩存更好的發揮作用。當數據大小不超過39字節的時候,會使用embstr編碼方式,當embstr編碼方式的數據進行疼和修改操作的時候,數據都會自動變成raw編碼方式。

2. 散列類型,散列類型就是java中的HshMap類型,它是一種字典結構,它存儲的是字段與字段值的映射,需要注意的是,散列類型不能前臺其他數據類型,也就是說,字段值只能是字符串類型(redis中的所有數據類型都不支持嵌套)。下面我們來簡單介紹一下散列數據類型:

    散列數據類型相對於傳統的關係型數據庫,看他有很大的優勢,比如,我現在要存儲幾個book對象,那麼再關係型數據庫中我就要建立一張book表,包括booknam、bookprice、bookauthor桑格字段,現在想爲id是1的book對象增加一個出版日期,那麼對於其他book對象,這個日期字段就是一種空間浪費,再後來,數據越來越多,不同的對象需要不同的屬性的時候,表的字段會越來越多,難於維護,同時,在修改表的結構的時候,必須中斷服務。而在redis的散列中,對於book對象+三個屬性的結構,redis不會強行要求每條記錄都依據這種結構,我們可以完全自由地增減字段而不影響其他記錄。如圖所示,我存儲了兩個book對象,第一個有三個字段,第二個有四個字段:

    散列類型主要有一下集中常用命令:

         HSET K field value:存儲一個字段和字段值

         HGET K field:獲取一個字段內容

         HMSET K field value [field value ...]:存儲多個字段和字段值

         HMGET K field [field ...]:獲取多個字段值

         HGETALL K:獲取所有的字段與對應字段值

         HEXISTS K field :判斷字段是否存在(存在返回1,否則返回0)

         HSETNX K field value:當字段不存在的時候賦值(相當於先判斷再賦值,不過這條命令是原子操作,沒有競爭問題存在)

         HDEL K field [field...]:刪除多條字段

         HKEYS K:只獲取k的所有字段名

         HVALS K:只獲取k的所有字段值

    散列類型的數據有兩種編碼方式:分別是:REDIS_ENCODING_HT(ht)和 REDIS_ENCODING_ZIPLIST(ziplist),兩種編碼方式是根絕配置文件設定的規則進行切換,打開.conf文件,找到如圖所示的地方:

可以看到兩條數據,分別是hash-max-ziplist-entries 512  和 hash-max-ziplist-value 64,它的意思就是當字段個數少於hash-max-ziplist-entries並且每個字段名和字段值 的長度都小於hash-max-ziplist-value(單位是字節)的時候,就會觸發ziplist編碼,否則使用ht編碼,redis的散列數據每次更變都會進行判斷。

首先,對於ht編碼方式,字段(注意不是鍵)和字段值都是使用redisObject進行存儲的,因此,對於字符串類型的編碼優化對也使用與散列的字段與字段值的優化。

ziplist編碼方式是一種緊湊的編碼格式,犧牲性能換取空間的思想,因此使用與元素較少的情況,它的結構如圖所示:

簡略的畫了一下,他主要由五部分組成,整個表的最上面是zlbytes,他記錄的是表所佔據的空間大小,zltail表示到最後一個元素的偏移量(可以直接定位到尾部無需遍歷),zllen指的是存儲元素的數量,元素是以特定的結構存儲的字段名以及字段值,最後zlend是一個值位255的單字節標識。其編碼優化主要發生再元素內部,我們再來看看每個元素內部的結構:

可以看到,每個元素是由四個部分組成的,第一個部分記錄前一個元素大小,以實現倒序查找,當前一個元素大小小於254字節的時候,佔用1個字節,否則佔用5個字節;第二部分記錄元素編碼方式,第三部分記錄元素大小,二三部分共同進行編碼優化,具體策略是:

      元素大小:  <= 63字節    兩部分佔1字節

                            >63字節 & <=16383字節   兩部分佔2字節

                            >16383字節   兩部分佔5字節

第四部分是記錄元素內容,他又自己的優化策略,如果內容可以轉換成數字的話,則會進行轉換來節省空間。

觀察上面兩張圖可以知道,散列再使用ziplist進行編碼的時候,字段與字段名是按順序存儲再元素中的。

從性能上講,ziplist的性能比較差,再查詢數據的時候,從第一個元素開始,每次跳過一個元素(略去字段值),因此,查找一個元素需要對錶進行遍歷,效率比較低,因此,再修改配置文件的時候,建議不要將參數hash-max-ziplist-entries   和 hash-max-ziplist-value設置的過大。

3. 列表類型,列表類型就是List數據類型,它的特點是有序可重複,其內部是使用雙向鏈表實現的,因此,你可以從頭部或者尾部對數據進行操作,在命令裏面,分爲左側操作和右側操作,其時間複雜度是Q(1),兩端的數據獲取的快,同時,通過索引獲取數據就很慢。對與實際開發中的應用場景,比如在一些社交網站上,人們只關心最新的內容,使用列表數據類型進行存儲,即使新鮮事的總數達到了上千萬個,獲取其中最新的100條數據也是極快的,他額的另外一個左右就是實現隊列。同時需要注意的就是,散列類型和列表類型對於最大字段數量是一樣的,都是(2^23)-1個元素。列表類型由如下幾種常用的命令:

         LPUSH key value [value...]

         RPUSH key value [value...]

上面兩個命令是向列表中添加數據,我們上面說過,這種數據類型是雙向的,所以它可以對列表的兩側進行操作,藉助於它的命令,我們不妨將他的兩側分成左端和右端,因此,以上兩個命令就可以理解爲LPUSH就是向左側添加數據,RPUSH就是向右側添加數據,這兩個命令的返回值就是列表的長度。如圖所示我建立了兩個列表類型的數據分別是names和numbers,names是使用lpush,numbers是使用rpush:

我們以lpush爲例,我們可以將列表想象成一個橫放在你面前的子彈夾,開口在左端,你每次使用lpush存放一個數據都類似於向子彈夾中添加一個子彈,新的子彈會不斷地從左側進入彈夾,同時將已經進去的子彈向右側頂,這樣,最後添加完成了,最早添加的子彈應該是在最右側。

        LPOP key

         RPOP key

這兩個命令就是分別從左右兩側取數據,我們依然可以將它理解成子彈夾,只不過是卸子彈的過程,LPOP的時候,是從左側取數據。

         LINE key

         LRANGE key start stop

         LREM key count value

         LTRIM key start stop

上面三個命令也十分常用,同時需要注意的地方也較多。LINE是獲取列表的長度,也就是元素個數,雖然它與SQL語句的SELECT COUNT(*)類似,但是它的時間複雜度是O(1),使用的時候直接讀取現成的值,無需遍歷;

LRANGE是截取列表片段,索引是從0開始,這裏面需要注意的由一下兩點:首先,這個命令不會像LPOP或者RPOP那樣刪除列表數據,只是讀取,另外一點就是截取範圍是包括最右側的數據的,LRANGE還有一個非常常用的技巧,就是將索引設置成0 -1,則會返回所有元素。另外,LRANGE支持負索引,當你的命令參數是負數的時候,標識從右側開始計算,-1是最右側的元素,-2標識右側第二個元素,但是在寫的時候,LRANGE的start位置永遠在stop位置的左側!否則返回空。

下一個LREM命令就是刪除列表中的指定數據,他的意思就是會刪除列表前count個元素中值是value的元素,注意:當count大於0的時候,是從左側開始計算,count小於0的時候是從右側開始計算,count如果等於0,則刪除全部數據。

最後一個LTRIM就死截取字符串的意思,相當於java中的trim()方法,這個沒什麼好說的。

         LINDEX key index

         LSET key index value

INDEX命令用於返回指定索引的元素,索引從0開始,如果是負數則是從右側開始計算。LSET命令則是將指定索引位置的元素內容替換成value,注意這是替換,原來的內容會刪除。

        LINSERT key BEFORE|AFTER pivot value

這個命令看起來有些複雜,它的作用就是像列表的指定數據前後插入新的數據,它的規則是這樣的:首先,他會在列表中從左到右尋找值爲pivot的元素,然後根據參數2來確定是在這個元素前面插入還是後面插入。返回值是插入後列表元素的個數。我們舉個例子:

可以看到,numbers裏面一共由五個數據,最左邊的是5,最右邊的是1,我先後存儲了兩個數據在3的兩側,可以看到after是指目標元素的右側,before是指目標元素的左側。

最後一個命令:

       RPOPLPUSH source destination

這是一個用於轉移數據的命令,它的作用就是將元素從一個列表中轉移到另一個列表中,我們可以對他的動作進行拆分:首先,在原始列表中執行RPOP命令,彈出一個元素,然後LPUSH將這個元素加入到目標列表的左側,返回值是被操作的元素,舉個例子就看明白了:

可以看到,我準備了兩個list,list1:d c b a   list2:4 3 2 1,我的操作就是將list1中的右側數據轉移到list2的左側,結果可以看出,最右側元素a已經跑到了list2的最左側。這個命令可以實現在多個隊列中傳遞數據;當目標列表與源列表相同時,會不斷的將隊尾數據移動到隊首。

好了列表數據類型簡單的介紹了一下,下面開始說說redis對他的編碼優化吧。列表類型有兩種編碼方式,分別是 REDIS_ENCODING_LINKDELIST和REDIS_ENCODING_ZIPLIST,與散列類型一樣,在配置文件中,依然可以定義REDIS_ENCODING_ZIPLIST的編碼時機,這裏不再說了。首先REDIS_ENCODING_LINKDELIST編碼方式即雙向鏈表,鏈表中的每個元素都是使用redisObject類型存儲的,鎖以他的優化方式與String類型一樣;而對於好了列表數據類型簡單的介紹了一下,下面開始說說redis對他的編碼優化吧。列表類型有兩種編碼方式,分別是 REDIS_ENCODING_LINKDELIST和REDIS_ENCODING_ZIPLIST編碼方式,其具體表現與散列一樣。對於redis後來增加的編碼方式:REDIS_ENCODING_QUICKLIST,他就是倆箇中編碼方式的結合版本,其原理就是將一個長列表分割成若干個以鏈表形式組織的ziplist,從而達到減少佔用空間的同時提升ziplist編碼性能的效果。

4. 集合類型

集合類型相當於java中的set,無序不重複,其內部是使用值爲空的散列實現的,所以它的時間複雜度都是O(1),集合之間可以進行常規的運算(並集、交集等)。集合常用的命令簡單的介紹一下:

        SADD key member [member...]

        SREM key member [member...]

這兩個命令很簡單,就是向集合中添加或者刪除元素,如果鍵不存在則會自動創建,返回值是操作成功的數量,同時,如果存儲的元素已經存在,由於集合類型是不重複的,所以會忽略,如圖所示:

srem就是刪除操作,可以一次性刪除多個數據,返回刪除成功的元素的個數,如果元素不存在則自動忽略。

        SMEMBERS key

這個命令是獲取幾個所有元素,我們接着上面的set2繼續操作:

                 SISMEMBER key member

這個命令是判斷一個元素是否存在與這個集合存在的時候返回1,不存在返回0.

        SDIFF key [key...]

        SINTER key [key...]

        SUNION key [key...]

這三個命令式進行集合之間的運算的,SDIFF命令是差集運算,什麼差集呢,舉個例子,我有{一個香蕉,一個蘋果,一個西瓜}(set不重複),你有{一個蘋果,一個桃子},那麼我 - 你 = {西瓜,香蕉},ok,我們就以這個使用一下命令看看:

很好理解吧,需要注意的是它支持多多鍵計算,計算順序是從左到右;

下一個命令SINTER,它是交集運算,就是兩個集合的公共部分,還是用上面的例子:

我和你都擁有的水果就是apple,這個命令也支持多鍵運算;

SUNION命令是並集運算,就是將兩個集合的元素合併起來,去除想用的元素後的集合,看例子:

        SCARD key

這個命令是獲取一個集合中的元素個數;

        SDIFFSTORE destination key [key...]

        SINTERSTORE destination key [key...]

        SUNIONSTORE destination key [key...]

進行集合運算並將結果存儲在destination鍵中,舉個例子:

        SRANDMEMBER key [count]

從集合中隨機獲取count個不同的元素,如果count爲負數的時候,會默認取count的絕對值;

        SPOP key

從集合中隨機彈出一個元素,彈出後這個元素將不存在與集合。

集合類型的編碼方式有兩種:REDIS_ENCODING_HT和REDIS_ENCODING_INTSET,轉換機制就是當集合中的所有元素都是整數並且元素個數小於配置文件中的

set-max-intset-entries參數指定的數的時候會使用REDIS_ENCODING_INTSET編碼方式,否則使用REDIS_ENCODING_HT來存儲。

首先,REDIS_ENCODING_INTSET編碼方式中定義了一個結構體intset:

 

 

其中,我們集合中的數據實際上就是存儲在contents裏面,其佔用空間大小與具體的編碼方式有關,默認的encoding是INTSET_ENC_INT16(2字節),如果增加的元素超過兩個字節,那麼redis自動將其升級爲INTSET_ENC_INT32(4字節),並調整整個集合的位置與長度,還可以升級到8字節;需要注意的是,REDIS_ENCODING_INTSET編碼方式是進行有序存儲的,因此在smembers輸出的元素是有序的,因此也就可以使用二分法查找元素,性能會隨着元素的增加而變差。

注意,一旦變成了INTSET編碼,redis不會自動的還原成原來的編碼方式。

5. 有序集合

顧名思義,它與集合類型的區別就在於是否有序,有序集合在集合的基礎上新增加了一個分數,爲每個元素關聯這個分數後,可以根據分數排序,注意:有序集合依然是不允許集合元素相同,但是分數可以相同!起常用命令如下:

        ZADD key score member [score member...]

該命令就是增加元素,如果元素已經存在,則會對分數進行替換,返回值是新增元素的個數(不包含已經存在的元素),同時,分數支持雙精度浮點數

這是插入完數據後的class信息,可以看到每一個元素都有一個關聯的分數。

        ZSCORE key member

獲取元素的分數

        ZRANGE key start stop [winthscores]

        ZREVRANGE key start dstop [WITHSCORES]

ZRANGE命令會在索引範圍中按照元素的分數從小到大返回元素,支持使用0 -1方式返回全部。後面的withscore可以獲取元素的同時加上分數

如圖,輸出的元素不僅有順序,而且還有分數。 ZREVRANGE命令則是以從大到小的方式排序,其他與zrange一樣。

        ZRANGEBYSCORE key  min max [withscores] [limit offset count]

這個命令的作用就是按照從小到大的順序獲取指定範圍分數的元素,min 和max就是範圍遍及額,默認是包含邊界的,但是可以使用“(”來忽略邊界元素,舉個例子:

如圖所示,我查詢的是85到95之間的數據,可以看到結果從大到小輸出出來,並且我在第二次查詢的時候使用了withscores來連帶輸出元素關聯的分數。

需要注意的是,使用limit offset count,他的作用就是將返回的結果從第offset個元素開始截取count個,比如,我現在用zrange獲取了四個元素1,2,3,4,如果語句後面有limit 1 2,那麼就是從索引是1的元素開始,獲取前2兩個元素就是2,3,索引從0開始!這裏就不舉例了。

        ZINCREBY key increment member

這個命令的作用就是增加某個元素的分數,返回值是增加後的分數。increment可以使負數,就是減的意思。

        ZCARD key 

        ZCOUNT key min max

ZCARD 獲取集合中元素的數量

ZCOUNT則是獲取分數指定範圍內的元素的個數

        ZREM key member [member...]

刪除一個或者多個元素,返回值是成功刪除的元素的個數,如果不加元素,則會將key中所有元素刪除。

        ZREMRANGEBYRANK key start stop

        ZREMRANGEBYSCORE key min max

ZREMRANGEBYRANK 刪除排名範圍內的元素,它首先會對元素進行從小到大的排序,然後按照給定的索引範圍進行刪除,索引從0開始。返回值是刪除元素的數量。

ZREMRANGEBYSCORE 則是根據分數範圍來刪除範圍內的元素。

        ZRANK key member

        ZREVRANK key member

ZRANK 命令按照分數從小到大的順序獲取元素的排名

 ZREVRANK則是從大到小

有序集合的編碼方式有兩種:REDIS_ENCODING_SKIPLIST 和 REDIS_ENCODING_ZIPLIST,ziplist的啓動時機也是在配置文件中設置,其按照“元素1的值”,“元素1的分數”,“元素2的值”,“元素2的分數”...的結構存儲的。其優化規則與散列、列表類型一樣;

使用SKIPLIST編碼方式的時候,redis會使用散列表與跳躍列表兩種數據結構存儲有序集合,其中散列表用來存儲元素與分數之間的映射關係,跳躍列表則用來存儲元素的分數到其元素之的映射以實現排序功能,採用此種編碼方式的時候,元素值是用redisObject存儲的,優化方式與string一樣,元素的分數是使用double類型存儲的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章