一、redis簡介 一般學習,最好先去官網,之所以建議看官網,是因為這是一手的學習資料,其他資料都最多只能算二手,一手資料意味著最權威,準確性最高。https://redis.io/topics/introduction。如果像我一樣,英語不好的童鞋,不要緊,咋們用Chrome瀏覽器,翻譯成中文。E ...
一、redis簡介
一般學習,最好先去官網,之所以建議看官網,是因為這是一手的學習資料,其他資料都最多只能算二手,一手資料意味著最權威,準確性最高。https://redis.io/topics/introduction。如果像我一樣,英語不好的童鞋,不要緊,咋們用Chrome瀏覽器,翻譯成中文。Eumm。。。來看看官網給的解釋:“redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.” ,第一句就告訴我們,redis是什麼:redis是一個開源的,基於記憶體的數據結構存儲,可用作於資料庫、緩存、消息中間件。
1.1、為什麼使用redis?
由官網可知:redis是基於記憶體,常用於緩存的一種技術,並redis存儲方式是以 Key-value 的形式。等等?Key-value??這個不就是Java中Map容器的特性嗎?那為什麼還用redis呢?
- Java實現的Map是本地緩存,只能存在創建他的程式中。最主要的特點是輕量以及快速。而且實例多的情況下,每個實例都需要各自保存一份緩存,緩存不具有一致性。
- redis實現的是分散式緩存,如果有多個實例機器,每個實例共用一份緩存,緩存具有一致性。
- Java中Map不是專業做緩存的,JVM記憶體太大容易掛掉,所以一般用來做容器存儲臨時數據,緩存隨著JVM銷毀而結束。
- redis是專門做緩存的,緩存可以持久化,可以將緩存的數據保存在硬碟中,redis重啟之後就可以恢復。但是Map是記憶體對象,程式重啟數據就沒有了。
- redis可以處理每秒百萬級的併發,Map只是一個普通的對象。
1.2、為什麼要用緩存
如果我們的網站出現了性能問題(訪問時間慢),一般是由於資料庫扛不住了。因為一般的資料庫的讀寫都是經過磁碟的,磁碟的讀寫相當於記憶體來說非常慢了。參考資料:讓CPU告訴你硬碟和網路到底有多慢:https://cizixs.com/2017/01/03/how-slow-is-disk-and-network/。用過Mybatis和Hibernate的同學都知道,他們有一級緩存、二級緩存這樣的功能(實質就是本地緩存),目的就是為了不用每次讀取數據的時候,都去資料庫查詢。
二、redis的對象和數據結構
註:本篇博文不講述redis命令的使用方式,具體的使用請查看API.(redis命令參考:http://doc.redisfans.com/)
【對象】
redis使用對象來表示資料庫中的鍵和值,每次在redis中新建一個鍵值對的時候,至少會創建出兩個對象。一個對象用作鍵值對的鍵(鍵對象),一個對象用作鍵值對的(值對象)。redis中的每種對象都由對象結構(redisObject) 與對應編碼的 數據結構 組合而成,redis支持5種對象類型,分別是字元串(string)、列表(list)、哈希(hash)、集合(set)、有序集合(zset),而每種對象類型至少對應兩種編碼方式,不同的編碼方式所對應的底層數據結構是不同的。
每個對象會用到的編碼以及對應的數據結構詳見下表:
每種對象對應兩至三種編碼,除skiplist編碼需要用到兩種數據結構(字典+跳躍表)外,其餘編碼均用到一種底層的數據結構。同一個對象類型,在不同的場景下用到的編碼(數據結構)不同,redis支持8種編碼以及8種底層的數據結構。這種方式更加靈活,可以幫助redis獲得更高的性能以及儘量占用更少的記憶體。比如如果字元串對象中要存儲的字元串內容所占位元組較小,會用embstr編碼的格式,如果要存儲的內容所占位元組較大,會用raw編碼的格式,具體細節後文會詳細說明。
上面說過,redis中的鍵和值都是由對象組成的,而對象是由對象結構和數據結構共同組成的。redis中的鍵,都是用字元串來存儲的,即對於redis資料庫中的鍵值對來說,鍵總是一個字元串對象,而值可以是字元串對象、列表對象、哈希對象、集合對象或者有序集合對象中的其中一種。
鍵、值的整體大致結構可以如下圖所示
【對象結構】
對象結構(redisObject)共有5個屬性,分別是type屬性、encoding屬性、ptr屬性、refcount屬性、lru屬性。
其中type屬性、encoding屬性、ptr屬性和保存數據有關
type屬性:表示該對象的類型是什麼
encoding屬性:表示這個對象使用的底層數據結構是什麼
ptr屬性:是一個指向底層數據結構的指針
refcount屬性是一個引用計數屬性,可以用於記憶體回收和對象共用
lru屬性,記錄了對象最後一次被命令程式訪問的時間,可以計算出某個鍵的空轉時長
對象結構的邏輯圖如下所示:
【記憶體回收--refcount屬性】
在對象結構中,有refcount這個屬性,該屬性用於記錄對象的引用計數信息,redis利用引用計數(reference counting)技術實現記憶體回收機制,通過這一機制,程式可以通過跟蹤對象的引用計數信息,在適當的時候自動釋放對象併進行記憶體回收。
具體策略:
在創建一個新對象時,引用計數的值會被初始化為1
當對象被一個新程式使用時,它的引用計數值會被增一
當對象不再被一個程式使用時,它的引用計數值會被減一
當對象的引用計數值變為0時,對象所占用的記憶體會被釋放
【對象共用--refcount屬性】
redis會在初始化伺服器時,伺服器會創建一萬個字元串對象,這些對象包含了從0到9999的所有整數值,當伺服器、新創建的鍵需要用到值為0到9999的字元串對象時,伺服器就會使用這些共用對象,而不是新創建對象。
對象結構中,refcount是引用指針屬性,如果有N個鍵共用一個值,refcount對應的值就為N。創建共用字元串對象的數量可以通過redis.h/redis_shared_intengers常量來修改。object refcount命令可以查看某個鍵對應的值被引用了多少次。
讓多個鍵共用一個值,需要執行以下兩個步驟:
將鍵的值指針,指向被共用的值對象
被共用的值對象的引用計數器加一,即refcount屬性的值加一,引用數為2的共用對象結構圖如下圖所示:
【進一步說明】
當伺服器考慮將一個鍵的值引用共用對象時,鍵的值作為目標對象,程式需要先檢查共用對象和目標對象的類型是否完全相同,只有在完全相同的情況下,共用對象才會被引用。而一個共用對象保存的值越複雜,驗證共用對象與目標對象所需的複雜度就會越高,消耗的CPU時間也會越多。
所以共用對象的優點是被其它鍵引用時,可以節省記憶體空間,缺點是被引用時需要進行判斷,這個過程需要消耗CPU,如果共用對象簡單,消耗很小的CPU並節省記憶體空間是值得的。但如果對象很複雜,進行判斷就需要消耗大量CPU,消耗大量CPU去節省記憶體空間是不值得的,因為redis本身的記憶體空間還是很大的。
redis支持5種對象,包括字元串對象、列表對象、哈希對象、集合對象以及有序集合對象。而字元串對象是redis中的一個基礎對象,其它對象均可以在底層的數據結構內部嵌套字元串對象。
對象共用:
1、只有字元串對象才能被創建為共用對象,被其它字元串鍵使用;
2、用字元串對象創建的共用對象,不單單隻有字元串鍵可以使用,那些在數據結構中嵌套了字元串對象的對象(linkedlist編碼的列表對象、hashtable編碼的哈希對象、hashtable編碼的集合對象,以及skiplist編碼的有序集合對象)都可以使用這些字元串共用對象。
【對象的空轉時長--lru屬性】
對象結構的lru屬性,記錄了對象最後一次被命令程式訪問的時間
空轉時長:當前時間減去鍵的值對象的lru時間,就是該鍵的空轉時長。Object idletime命令可以列印出給定鍵的空轉時長
如果伺服器打開了maxmemory選項,並且伺服器用於回收記憶體的演算法為volatile-lru或者allkeys-lru,那麼當伺服器占用的記憶體數超過了maxmemory選項所設置的上限值時,空轉時長較高的那部分鍵會優先被伺服器釋放,從而回收記憶體。
2.1、字元串對象
2.11、字元串對象介紹
字元串對象可以存儲整數、浮點數、字元串,具體策略是:
當存儲整數時,用到的編碼是int,底層的數據結構可以用來存儲long類型的整數
當存儲字元串時,如果字元串的長度小於等於32位元組,那麼將用編碼為embstr的格式來存儲;如果字元串的長度大於32位元組,將用編碼為raw的SDS格式來存儲
當存儲浮點數時會先將浮點數轉換為字元串,如果轉換後的字元串長度小於32位元組就用編碼為embstr的格式來存儲,否則用編碼為raw的SDS格式來存儲
下圖是以raw編碼的字元串對象結構圖,最左側是對象結構,中間跟右側合起來是raw編碼的SDS數據結構(sdshdr),示例圖:
2.12、raw編碼,簡單動態字元串(simple dynamic string-SDS)
雖然redis由C語言編寫,但是redis用的並不是C語言傳統的字元串,而是自己構建了簡單動態字元串(simple dynamic string,SDS)。當redis列印日誌信息或輸出報錯信息,這些輸出的字元串是不會被修改的字元串字面量(sting literal),此時用的是C語言傳統的字元串來存儲這些信息的。當redis需要存儲的是可以被修改的字元串時,就會使用SDS結構。除了用來保存資料庫中的字元串值之外,SDS還被用作緩衝區(buffer):AOF模塊中的AOF緩衝區,以及客戶端狀態中的輸入緩衝區,都是由SDS實現的。
redis使用sdshdr結構來表示一個SDS的值,SDS結構示意圖如下:
sdshdr是該數據結構的名稱即SDS,其中:
buf屬性,是一個位元組數組,用來保存字元串,後面箭頭對應的就是實際保存的字元串內容,最後以’\0’空字元串結尾
len屬性,記錄的是buf數組中實際已使用的位元組數量,等於SDS所保存字元串的長度
free屬性,記錄的是buf數組中未存儲內容的空餘大小,單位位元組
2.1.3、使用SDS的好處
一、可以用O(1)的複雜度獲取到字元串長度
SDS的len屬性記錄了字元串的長度,而傳統C字元串要想知道長度需要遍歷整個字元串。相比於傳統C字元串,redis獲取字元串長度所需的複雜度從O(N)降低到了O(1)。
即使對非常長的字元串反覆執行STRLEN命令(獲取字元串長度),也不會造成過多的性能消耗。
二、杜絕緩衝區溢出
在傳統的C字元串中,如果要修改字元串的內容,但修改後字元串的長度超過原先的長度就會發生溢出現象。詳見下圖:
在SDS中,當需要對buf位元組數組中存儲的內容進行修改(增添或刪除)時,API會先通過free和len屬性檢查SDS的空間是否足夠,如果不夠的話,SDS會自動擴展空間再對內容進行修改。關於自動擴展空間的策略見下方“空間預分配”的內容。
三、減少修改字元串長度時所需的記憶體重分配次數
對於傳統C字元串:
如果執行的是增長字元串的操作,如拼接操作(append),那麼在執行命令之前,程式需要先通過記憶體重分配來擴展底層數據的空間大小——否則會產生緩衝區溢出。
如果執行的是縮短字元串的操作,如截斷操作(trim),那麼在執行這個操作之後,程式需要通過記憶體重分配來釋放字元串不再使用的空間——否則會產生記憶體泄漏。
對於redis中的SDS結構:
記憶體重分配設計複雜的演算法,是一個比較耗時的操作,redis作為速度要求嚴苛、數據會被頻繁執行的資料庫,如果每次修改字元串都需要進行一次記憶體重分配,會嚴重影響性能。
使用SDS,buf數組裡可以包含未使用的位元組,這些位元組的數量由free屬性記錄,可以減少修改字元串長度時所需的記憶體重分配次數。
【空間預分配和惰性空間釋放】
通過SDS中free屬性定義的未使用空間,SDS可以實現空間預分配和惰性空間釋放兩種優化策略:
1、空間預分配策略——可以降低字元串增長操作引起的記憶體重分配
當需要修改SDS的內容,且需要進行空間擴展的時候,程式不僅會為SDS分配修改所需的必須空間,還會為SDS分配額外的未使用空間。
其中,額外分配的未使用空間數量由以下公式決定:
如果對SDS進行修改之後,SDS的長度(即len屬性的值)將小於1MB,那麼程式將分配和len屬性同樣大小的未使用空間,這時SDS len屬性的值將和free屬性的值相同。
如果對SDS進行修改後,SDS的長度將大於等於1MB,那麼程式會分配1MB的未使用空間。
【進一步說明】
如果對一個字元串的末尾持續追加內容,當字元串整體大小大於1MB時,即使只追加一位元組的字元,程式也會額外分配1MB的空間,當再次追加一位元組的字元時,程式不會再額外分配1MB的空間,而是使用已有的空閑空間。
即在擴展空間之前,會先檢查未使用的空間是否足夠,如果足夠,是不會額外再擴展的
通過空間預分配策略,SDS將連續增長N次字元串所需的記憶體重分配次數從必定N次降低為最多N次。
2、惰性空間釋放策略——可以降低字元串縮短操作引起的記憶體重分配
當SDS中的字元串長度被縮短時,程式並不會立即使用記憶體重分配來回收縮短後多出來的位元組空間,而是使用free屬性將這些位元組的數量記錄起來,以備將來使用。
當然,redis提供了相應的命令來真正釋放這些未使用空間,避免不必要的記憶體浪費。
四、二進位安全
C字元串中的字元必須符合某種編碼(比如ASCII),並且除了字元串的末尾之外,字元串裡面不能包含空字元,如果字元串除末尾外還有其它空字元,那麼最先被程式讀入的空字元將被誤認為是字元串結尾,這些限制使得C字元串只能保存文本數據,而不能保存圖片、音頻、視頻、壓縮文件這樣的二進位數據。
為了確保redis可以適用於各種不同的使用場景,SDS的API都是二進位安全的(binary-safe),所有SDS API都會以處理二進位的方式來處理SDS存放buf數組裡的數據,程式不會對其中的數據做任何限制、過濾或者假設,數據在寫入時是什麼樣的,它被讀取時就是什麼樣。
這也是SDS的buf屬性被稱為位元組數組的原因——redis不是用這個數組來保存字元,而是用它來保存一系列二進位數據。
五、相容部分C字元串函數
SDS遵循空字元串結尾這一慣例,好處是可以直接重用C字元串函數庫里的函數,從而避免了不必要的代碼重覆
【embstr編碼】
如果字元串對象保存的是長度小於等於32位元組的字元串,那麼將會使用embstr編碼,embstr編碼是專門用來保存短字元串的一種優化編碼方式。embstr編碼與raw編碼對應的字元串對象,都是由對象結構(redisObject)和數據結構(sdshdr)組成的。
區別在於用raw編碼的字元串對象會調用兩次記憶體分配函數來分別創建redisObject結構和sdshdr結構,而embstr編碼則通過調用一次記憶體分配函數來分配一塊連續的空間,空間中一次包含redisObject和sdshr兩個結構,embstr編碼的字元串對象結構圖如下所示:
兩者的區別
embstr編碼的字元串對象在執行命令時,產生的效果和raw編碼的字元串對象執行命令時產生的效果是相同的,但使用embstr編碼的字元串對象來保存短字元串值有以下好處:
1、embstr編碼將創建字元串對象所需的記憶體分配次數從raw編碼的兩次降低為一次
2、釋放embstr編碼的字元串對象只需要調用一次記憶體釋放函數,而釋放raw編碼的字元串對象需要調用兩次記憶體釋放函數
3、embstr編碼的字元串對象的所有數據都保存在一塊連續的記憶體里,結構更加緊湊,而raw編碼是分散開的,redisObject對象結構和sdshdr數據結構彼此間是用指針相關聯的,embstr編碼的對象比raw編碼的對象能夠更好的利用緩存帶來的優勢。
【編碼的轉換】
int編碼的字元串對象和embstr編碼的字元串對象在條件滿足的情況下,會被轉換成raw編碼的字元串對象。encoding命令可以查看鍵對應的值,底層用的是什麼編碼。
int轉換為raw:
對於int編碼的字元串對象來說,如果我們向對象執行了一些命令,使得這個對象保存的不再是整數值,而是一個字元串值,那麼字元串對象的編碼將從int變為raw
127.0.0.1:6379> set a 100 OK 127.0.0.1:6379> object encoding a "int" 127.0.0.1:6379> append a 'a' (integer) 4 127.0.0.1:6379> get a "100a" 127.0.0.1:6379> object encoding a "raw"
int編碼的字元串,存儲的是long類型的整數,範圍是2^63-1(2的63次方減一) ~ -2^63(2的63次方),當存儲的整數在該範圍內時,編碼為int,當值超過該範圍,編碼將轉換為embstr
127.0.0.1:6379> set number1 9223372036854775807 OK 127.0.0.1:6379> object encoding number1 "int" 127.0.0.1:6379> set number1 9223372036854775808 OK 127.0.0.1:6379> object encoding number1 "embstr" 127.0.0.1:6379> set number -9223372036854775808 OK 127.0.0.1:6379> object encoding number "int" 127.0.0.1:6379> set number -9223372036854775809 OK 127.0.0.1:6379> object encoding number "embstr"
embstr轉換為raw:
embstr編碼的字元串對象無法被修改(redis沒有為embstr編碼的字元串對象編寫任何響應的修改程式),只有int、raw編碼的字元串對象可以被修改,所以embstr編碼的字元串實際上是只讀的。
當對embstr編碼的字元串對象執行任何修改命令時,程式都會先將對象的編碼從embstr轉換為raw,然後再執行修改命令。所以一旦embstr編碼的字元串被修改,它的數據結構就會變成raw編碼的格式。
127.0.0.1:6379> set a 'ab' OK 127.0.0.1:6379> object encoding a "embstr" 127.0.0.1:6379> append a 'c' (integer) 3 127.0.0.1:6379> get a "abc" 127.0.0.1:6379> object encoding a "raw"