通常學習,最好先去官網,之因此建議看官網,是由於這是一手的學習資料,其餘資料都最多隻能算二手,一手資料意味着最權威,準確性最高。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是一個開源的,基於內存的數據結構存儲,可用做於數據庫、緩存、消息中間件。html
由官網可知:redis是基於內存,經常使用於緩存的一種技術,並redis存儲方式是以 Key-value 的形式。等等?Key-value??這個不就是Java中Map容器的特性嗎?那爲何還用redis呢?redis
若是咱們的網站出現了性能問題(訪問時間慢),通常是因爲數據庫扛不住了。由於通常的數據庫的讀寫都是通過磁盤的,磁盤的讀寫至關於內存來講很是慢了。參考資料:讓CPU告訴你硬盤和網絡到底有多慢:https://cizixs.com/2017/01/03/how-slow-is-disk-and-network/。用過Mybatis和Hibernate的同窗都知道,他們有一級緩存、二級緩存這樣的功能(實質就是本地緩存),目的就是爲了不用每次讀取數據的時候,都去數據庫查詢。算法
注:本篇博文不講述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這個屬性,該屬性用於記錄對象的引用計數信息,redis利用引用計數(reference counting)技術實現內存回收機制,經過這一機制,程序能夠經過跟蹤對象的引用計數信息,在適當的時候自動釋放對象並進行內存回收。
具體策略:
在建立一個新對象時,引用計數的值會被初始化爲1
當對象被一個新程序使用時,它的引用計數值會被增一
當對象再也不被一個程序使用時,它的引用計數值會被減一
當對象的引用計數值變爲0時,對象所佔用的內存會被釋放
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中的一個基礎對象,其它對象都可以在底層的數據結構內部嵌套字符串對象。
對象共享:
一、只有字符串對象才能被建立爲共享對象,被其它字符串鍵使用;
二、用字符串對象建立的共享對象,不僅僅只有字符串鍵可使用,那些在數據結構中嵌套了字符串對象的對象(linkedlist編碼的列表對象、hashtable編碼的哈希對象、hashtable編碼的集合對象,以及skiplist編碼的有序集合對象)均可以使用這些字符串共享對象。
對象結構的lru屬性,記錄了對象最後一次被命令程序訪問的時間
空轉時長:當前時間減去鍵的值對象的lru時間,就是該鍵的空轉時長。Object idletime命令能夠打印出給定鍵的空轉時長
若是服務器打開了maxmemory選項,而且服務器用於回收內存的算法爲volatile-lru或者allkeys-lru,那麼當服務器佔用的內存數超過了maxmemory選項所設置的上限值時,空轉時長較高的那部分鍵會優先被服務器釋放,從而回收內存。
字符串對象能夠存儲整數、浮點數、字符串,具體策略是:
當存儲整數時,用到的編碼是int,底層的數據結構能夠用來存儲long類型的整數
當存儲字符串時,若是字符串的長度小於等於32字節,那麼將用編碼爲embstr的格式來存儲;若是字符串的長度大於32字節,將用編碼爲raw的SDS格式來存儲
當存儲浮點數時會先將浮點數轉換爲字符串,若是轉換後的字符串長度小於32字節就用編碼爲embstr的格式來存儲,不然用編碼爲raw的SDS格式來存儲
下圖是以raw編碼的字符串對象結構圖,最左側是對象結構,中間跟右側合起來是raw編碼的SDS數據結構(sdshdr),示例圖:
雖然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數組中未存儲內容的空餘大小,單位字節
1、能夠用O(1)的複雜度獲取到字符串長度
SDS的len屬性記錄了字符串的長度,而傳統C字符串要想知道長度須要遍歷整個字符串。相比於傳統C字符串,redis獲取字符串長度所需的複雜度從O(N)下降到了O(1)。
即便對很是長的字符串反覆執行STRLEN命令(獲取字符串長度),也不會形成過多的性能消耗。
2、杜絕緩衝區溢出
在傳統的C字符串中,若是要修改字符串的內容,但修改後字符串的長度超過原先的長度就會發生溢出現象。詳見下圖:
在SDS中,當須要對buf字節數組中存儲的內容進行修改(增添或刪除)時,API會先經過free和len屬性檢查SDS的空間是否足夠,若是不夠的話,SDS會自動擴展空間再對內容進行修改。關於自動擴展空間的策略見下方「空間預分配」的內容。
3、減小修改字符串長度時所需的內存重分配次數
對於傳統C字符串:
若是執行的是增加字符串的操做,如拼接操做(append),那麼在執行命令以前,程序須要先經過內存重分配來擴展底層數據的空間大小——不然會產生緩衝區溢出。
若是執行的是縮短字符串的操做,如截斷操做(trim),那麼在執行這個操做以後,程序須要經過內存重分配來釋放字符串再也不使用的空間——不然會產生內存泄漏。
對於redis中的SDS結構:
內存重分配設計複雜的算法,是一個比較耗時的操做,redis做爲速度要求嚴苛、數據會被頻繁執行的數據庫,若是每次修改字符串都須要進行一次內存重分配,會嚴重影響性能。
使用SDS,buf數組裏能夠包含未使用的字節,這些字節的數量由free屬性記錄,能夠減小修改字符串長度時所需的內存重分配次數。
【空間預分配和惰性空間釋放】
經過SDS中free屬性定義的未使用空間,SDS能夠實現空間預分配和惰性空間釋放兩種優化策略:
一、空間預分配策略——能夠下降字符串增加操做引發的內存重分配
當須要修改SDS的內容,且須要進行空間擴展的時候,程序不只會爲SDS分配修改所需的必須空間,還會爲SDS分配額外的未使用空間。
其中,額外分配的未使用空間數量由如下公式決定:
若是對SDS進行修改以後,SDS的長度(即len屬性的值)將小於1MB,那麼程序將分配和len屬性一樣大小的未使用空間,這時SDS len屬性的值將和free屬性的值相同。
若是對SDS進行修改後,SDS的長度將大於等於1MB,那麼程序會分配1MB的未使用空間。
【進一步說明】
若是對一個字符串的末尾持續追加內容,當字符串總體大小大於1MB時,即便只追加一字節的字符,程序也會額外分配1MB的空間,當再次追加一字節的字符時,程序不會再額外分配1MB的空間,而是使用已有的空閒空間。
即在擴展空間以前,會先檢查未使用的空間是否足夠,若是足夠,是不會額外再擴展的
經過空間預分配策略,SDS將連續增加N次字符串所需的內存重分配次數從一定N次下降爲最多N次。
二、惰性空間釋放策略——能夠下降字符串縮短操做引發的內存重分配
當SDS中的字符串長度被縮短時,程序並不會當即使用內存重分配來回收縮短後多出來的字節空間,而是使用free屬性將這些字節的數量記錄起來,以備未來使用。
固然,redis提供了相應的命令來真正釋放這些未使用空間,避免沒必要要的內存浪費。
4、二進制安全
C字符串中的字符必須符合某種編碼(好比ASCII),而且除了字符串的末尾以外,字符串裏面不能包含空字符,若是字符串除末尾外還有其它空字符,那麼最早被程序讀入的空字符將被誤認爲是字符串結尾,這些限制使得C字符串只能保存文本數據,而不能保存圖片、音頻、視頻、壓縮文件這樣的二進制數據。
爲了確保redis能夠適用於各類不一樣的使用場景,SDS的API都是二進制安全的(binary-safe),全部SDS API都會以處理二進制的方式來處理SDS存放buf數組裏的數據,程序不會對其中的數據作任何限制、過濾或者假設,數據在寫入時是什麼樣的,它被讀取時就是什麼樣。
這也是SDS的buf屬性被稱爲字節數組的緣由——redis不是用這個數組來保存字符,而是用它來保存一系列二進制數據。
5、兼容部分C字符串函數
SDS遵循空字符串結尾這一慣例,好處是能夠直接重用C字符串函數庫裏的函數,從而避免了沒必要要的代碼重複
若是字符串對象保存的是長度小於等於32字節的字符串,那麼將會使用embstr編碼,embstr編碼是專門用來保存短字符串的一種優化編碼方式。embstr編碼與raw編碼對應的字符串對象,都是由對象結構(redisObject)和數據結構(sdshdr)組成的。
區別在於用raw編碼的字符串對象會調用兩次內存分配函數來分別建立redisObject結構和sdshdr結構,而embstr編碼則經過調用一次內存分配函數來分配一塊連續的空間,空間中一次包含redisObject和sdshr兩個結構,embstr編碼的字符串對象結構圖以下所示:
二者的區別
embstr編碼的字符串對象在執行命令時,產生的效果和raw編碼的字符串對象執行命令時產生的效果是相同的,但使用embstr編碼的字符串對象來保存短字符串值有如下好處:
一、embstr編碼將建立字符串對象所需的內存分配次數從raw編碼的兩次下降爲一次
二、釋放embstr編碼的字符串對象只須要調用一次內存釋放函數,而釋放raw編碼的字符串對象須要調用兩次內存釋放函數
三、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"