工具和中介軟體——redis,從底層原理到開發實踐
目錄
2.1 從“處理器-快取-記憶體”到“後臺-redis-資料庫”
2.3 redis典型問題:快取穿透、快取雪崩和快取擊穿(以淘寶雙11搶購為例)
3.1 redisObject物件(型別type+編碼encoding)和sds(free+len+buf)
5.3 Java使用redis叢集、Spring容器使用redis叢集(jedisCluster)
5.4 附加部分:關於centos上面的redis叢集節點操作(包括:新增主節點、新增從節點、刪除節點)
一、前言
redis引入,什麼是redis?
Redis 是一個開源的、使用ANSI C編寫的、支援網路、基於記憶體的、可持久化的Key-Value 型的資料庫,通過提供多種鍵值資料型別(5種基本資料型別 string hash list set sortedset)來適應不同場景下的儲存需求,並且提供多種語言的API(當然包括Java語言的API)。redis官網如圖:
理清幾個易混淆的概念
SQL:全稱為Structured Query Language,譯為結構化查詢語言,是一種計算機程式語言,一種解釋型語言。
NoSQL:全稱Not Only SQL,譯為"不僅僅是SQL"(注意,NoSQL不是不使用SQL的意思),泛指所有的非關係型資料庫。
關係型資料庫:即RDB,全稱為Relational Database,其實,一個更加常見的英文簡稱是RDBMS,Relational Database Management System,關係型資料庫管理系統,所以,RDBMS就被認為是關係型資料庫的簡稱。
非關係型資料庫:不使用資料庫表結構儲存資料,用NoSQL表示。
相互對比辨析相近概念
SQL與關係型資料庫:SQL是SQL,關係型資料庫是關係型資料庫,兩者是完全不同的兩個東西,SQL是一種解釋型語言,關係型資料庫是資料庫的一種型別,兩者的關係是關係型資料庫的CRUD操作使用SQL語言來完成,SQL語言被認為是關係型資料庫的一種特徵。
NoSQL與非關係型資料庫:對於程式設計師的工作中,NoSQL就是指非關係型資料庫,即NoSQL==非關係型資料庫,兩者是同一個東西。
SQL與NoSQL:SQL是一種語言,NoSQL表示非關係型資料庫,一個是語言,一個是資料庫,兩個不同關係。
關係型資料庫與非關係型資料庫:且看下錶
關係型資料庫RDBMS 非關係型資料庫NoSQL 儲存格式支援 資料庫表結構 不使用資料庫表結構儲存,包括列儲存、文件儲存、key-value儲存、圖儲存、物件儲存、xml儲存
特點 高度組織化結構化資料
結構化查詢語言(SQL) (SQL)
資料和關係都儲存在單獨的表中。
資料操縱語言,資料定義語言
嚴格的一致性
基礎事務代表著不僅僅是SQL
沒有宣告性查詢語言
沒有預定義的模式
-鍵 - 值對儲存,列儲存,文件儲存,圖形資料庫
最終一致性,而非ACID屬性
非結構化和不可預知的資料
CAP定理
高效能,高可用性和可伸縮性設計原則 ACID
A (Atomicity) 原子性、C (Consistency) 一致性、I (Isolation) 獨立性、D (Durability) 永續性
表示任何一個關係型資料庫(使用表格式儲存的資料庫)必須同時滿足四個特性要求
BASE原則(同時滿足CAP中的CA)
Basically Availble --基本可用
Soft-state --軟狀態/柔性事務。 "Soft state" 可以理解為"無連線"的, 而 "Hard state" 是"面向連線"的
Eventual Consistency -- 最終一致性, 也是是 ACID 的最終目的。
分類 Mysql sqlserver oracle
1)列儲存:按列儲存資料的,如Hbase、Cassandra、Hypertable
2)文件儲存:用類似json的格式儲存,儲存的內容是文件型的,如MongoDB、CouchDB
3)key-value儲存:Tokyo Cabinet / Tyrant
、Berkeley DB、MemcacheDB、Redis
4)圖儲存:圖形關係的最佳儲存,如Neo4J、FlockDB
5)物件儲存:通過物件的方式存取資料,如db4o、Versant
6)xml儲存:儲存XML資料,如Berkeley DB XML、BaseX
由上表可知,Redis是一種使用key-value鍵值對來儲存資料的非關係型資料庫。
redis與NoSQL的關係:NoSQL可以表示非關係型資料庫,redis一種使用key-value鍵值對儲存的非關係型資料庫,這就是兩者的關係。
本文主要包括四個部分的內容,包括redis基礎知識、redis底層原理、單機版redis及Java開發實踐、叢集版redis及Java開發實踐。
二、redis基礎知識
既然Redis是一種使用key-value鍵值對來儲存資料的非關係型資料庫,我們先來介紹這種非關係型資料庫的基礎知識。
2.1 從“處理器-快取-記憶體”到“後臺-redis-資料庫”
回顧學生年代《計算機組成原理》,由於處理器CPU與記憶體的速度不匹配問題,所有我們在處理器和記憶體之間加一個快取記憶體,
快取的資料是主存中熱點資料的副本,處理器讀取資料時,優先讀取快取中的資料,快取中沒有,再到主存中取,同時這個資料成為熱點資料,寫入到快取中,下次處理器直接從快取中取。
對於寫操作,為了保證主存快取中資料一致性問題,有“寫直達法”和“寫回法”兩種方式。
整個架構變化如圖:
工作中,web專案開發,由於網路請求與資料庫查詢資料不匹配問題,所以我們在後臺程式與資料庫之間加一個redis/redis-cluster快取,其讀寫操作與計算機的儲存一樣,優先讀寫redis快取,整個架構變化如下:
這裡用硬體對比軟體後臺,用快取記憶體cache對比redis/redis-cluster,兩者基本上是一樣的,唯一不同的恐怕就是Cache是硬體,redis是軟體。
2.2 不使用快取與使用快取(讀操作+寫操作)
在介紹redis讀寫之前,引入一個知識,redis支援的資料型別(我們起碼要知道讀寫操作,讀寫的是什麼)
redis支援五種資料型別
目前為止Redis支援的鍵值資料型別一共五種,如下:String字串型別、hash雜湊型別、list列表型別、set集合型別、sorted set有序集合型別。
不使用快取讀
不使用快取寫
使用快取讀
使用快取寫(先更新資料庫,再更新redis快取,類似寫直達法)
使用快取寫(先更新redis快取,再更新資料庫,類似寫回法)
2.3 redis典型問題:快取穿透、快取雪崩和快取擊穿(以淘寶雙11搶購為例)
2.3.1 快取穿透,不存在的商品X
從名稱上來解釋含義:傳統意義上的穿透,即水滴石穿,滴水能把石穿透,就是說水滴穿透石頭的整個過程。
這裡的快取穿透,是指網路請求查詢一個資料庫一定不存在的資料(假設為-1,一個無意義數字)。因為這是一個資料庫中絕對不存在的資料,所有redis快取中也一定不存在,執行過程中,因為redis中一定找不到,所有一定會去資料庫中找,結果就是資料庫也找不到。因為這個網路請求查詢過程是 “前端/客戶端/移動端---網路請求---redis快取---資料庫” ,整個過程穿透redis,直達資料庫,與水滴石穿有類似之意,所以稱為快取穿透。
正常的使用快取流程大致是,資料查詢先進行快取查詢,如果key不存在或者key已經過期,再對資料庫進行查詢,並把查詢到的物件,放進快取。如果資料庫查詢物件為空,則不放進快取(這是重點,查不到就不進入快取,所以第二次請求同樣的資料還是要查詢資料庫)。
快取穿透的問題再哪裡?在於它每次都要請求資料庫,redis快取形同虛設,起不到減少資料庫查詢、提升效能的作用。
我們知道,每一次查詢資料庫的代價是比較大的(所以我們引用了redis快取),因為請求的是一個資料庫一定不存在的資料,所有每一次都要查資料庫,而且因為資料庫查詢為空,這次的資料也不會放入快取,下一次還是查詢這個資料又要到資料庫中查詢,不斷迴圈,一個不存在的資料多次請求就可以讓後臺系統崩潰。
假如有惡意攻擊,就可以利用這個漏洞(網路請求一個資料庫中一定不存在的資料,不斷請求),對資料庫造成壓力,甚至壓垮資料庫。即便是採用UUID,也是很容易找到一個不存在的KEY,進行攻擊。
舉例:
解決方案:
思考:第一次請求redis中找不到,訪問資料庫不是什麼大問題,後面N-1次都要訪問資料庫這就是個大問題了。核心在於:如果資料庫查詢物件為空,則不放進快取。這是預設規則,如果能消除這條規則,即資料庫查詢為空也寫入redis,後面進直接從redis中取,取不到就結束(因為資料庫和redis已經同步了)。
解決:會採用快取空值null的方式,如果從資料庫查詢的物件為空,也放入快取,即將null放入快取,將key:value=(x,null)寫入redis,下一次查詢key=x,直接在redis中返回value=null.
2.3.2 快取雪崩,雙十一搶購
從名稱上來解釋含義:傳統意義上的雪崩就是指一種當山坡積雪內部的內聚力抗拒不了它所受到的重力拉引時,便向下滑動,引起大量雪體崩塌的自然現象。
雪崩之所以可怕,是因為其規模之大,區域性雪崩可能引起全域性雪崩,一次嚴重的雪崩可能造成整座雪山的崩塌。這裡的快取雪崩是指在某一個時間段,快取集中過期失效,造成整個redis不可用(對應整座雪山崩塌)。
關於快取雪崩,粗體標記,注意兩個詞語,一是“集中”,二是“過期”,一是集中失效,二是過期失效
關於集中:redis預設有16個庫,db0~db15,“集中”表示redis快取中的大部分庫是失效了
關於過期:表示redis快取雪崩中多個庫是由於快取時間到期而失效的
快取雪崩就是快取失效,“集中”告訴我們是大部分庫失效了,不是小部分或者個別;“過期”是指這種庫失效是由於快取時間到期而失效的(即是正常的失效),不是異常錯誤導致庫失效
舉例:產生雪崩的原因之一,以淘寶雙十一搶購為例,假設淘寶後臺將熱門商品放入redis/redis-cluster(像淘寶這麼大的肯定是redis-cluster redis叢集嘍),設定快取時間為一小時(當然淘寶系統不會如此愚蠢,這裡是假設,皮),那麼午夜12點開始搶購,到了午夜1點,所有的熱門商品的快取都過期了,如果使用者再購買商品,後臺就是讀寫資料庫(而不是直接讀寫redis)了,這樣造成訪問速度慢,帶來無法容忍的使用者體驗。如圖:
這樣的快取集體過期就是快取雪崩,是使用快取的一種危險,開發者一定要記住。
解決:
思考方式一:如果發生了集體快取過期,即快取雪崩,是非常可怕且很難挽救的,在發生後的一段時間內相當於沒有使用redis快取技術,退化為原始的持久層資料庫操作。所以,我們的思考不是發生快取雪崩之後如何解決,而是如何避免快取雪崩的發生。
解決方案一——過期錯開:將key的過期時間後面加上一個隨機數,讓key均勻的失效;或者使用一種特定的演算法,使過期時間賦值更符合實際業務。
思考方式二:第一種方案均攤過期時間或使用特定演算法,旨在最大程度在避免出現快取雪崩,但是如果快取雪崩確實發生了,程式如何應對呢?
解決方案二——排隊處理:使用優先佇列或者鎖讓程式執行在壓力範圍之內,如果訪問量達到閾值,排隊處理業務請求,即為了保證系統的不會崩潰,不要同時處理所有請求。
2.3.3 快取擊穿,iphoneX上市了
從名稱上來解釋含義:傳統意義上的擊穿(電壓擊穿)是指在電場作用下絕緣體內部產生破壞性的放電,絕緣電阻下降,電流增大,併產生破壞和穿孔的現象。
這裡的快取擊穿,是指一個key非常熱點,在不停的扛著大併發,大併發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大併發就穿破快取(類似電壓穿破絕緣體),直接請求資料庫,就像在一個屏障上鑿開了一個洞。
快取穿透與快取擊穿異同:
相同點:都是資料庫承受不了巨大壓力,導致崩潰。
不同點:
快取穿透是指redis沒作用,形同虛設,每一次訪問都要查詢資料庫,導致崩潰,這是技術上可以解決的,redis中記錄一個(x,null)鍵值對;
快取擊穿是指redis作用了,但是資料量實在是太大了,實在是承受不了這麼大的資料量,資料庫連帶redis快取一起崩潰,這時在固定的硬體成本下,快取、資料庫軟體方面已經達到理論上的最優了,技術上解決不了。
舉例:以iphoneX釋出為例,一下子就成了熱款,所有人通過淘寶線上購買,巨大的併發量某一時刻擊穿快取,直接請求資料庫,而資料庫又無法高速查表,導致系統崩潰。如圖:
解決方案
思考:現在的問題是資料量實在太大了,redis和資料庫的設計已經達到最優了。
步驟一:熱賣商品redis有效期設定為永久,絕對不要出現過期問題,redis方面達到最優。
步驟二:在固定的硬體成本下,資料庫(mysql或oracle)在表設計達到最優,框架(如mybatis)sql查詢語句設計達到最優
步驟三:設定一個優先佇列(如12306 買春運往返票),控制併發數,防止系統崩潰。
實際上,其實,大多數情況下這種爆款很難對資料庫伺服器造成壓垮性的壓力,能達到這種併發的可能也只有“12306春運購票”、“淘寶雙十一” 、“春晚跨年”這樣的事情了,從另外一個方面來講,如果真的有某個單一商品銷售量達到使用讓redis、資料庫崩潰,公司錢也賺了不少了,趕緊偷著笑吧!
三、redis五種型別的底層原理
3.1 redisObject物件(型別type+編碼encoding)和sds(free+len+buf)
這個很重要,要看懂後面五個型別的底層結構,要先搞懂redisObjet和sds(sdshdr)的結構
3.1.1 redisObject物件
Redis基於以上的資料結構建立了一個物件體系,包含了字串物件,列表物件,雜湊物件,集合物件,有序集合物件這五種物件.
Redis的物件體系還實現了基於引用計數技術的記憶體回收機制,同時基於引用計數技術實現了物件共享機制,在適當條件,通過多個資料庫鍵共享同一個物件來節約記憶體.
Redis中的每一個物件都由一個redisObject結構表示,這個redisObject物件結構中和儲存資料有關的三個屬性:type屬性、encoding屬性、ptr屬性,如下:
typedef struct redisObject {
//型別
unsigned type:4;
//編碼
unsigned encoding:4;
//指向底層實現資料結構的指標
void *ptr;
// ...
} robj;
下面分別對型別type、編碼encoding、指標ptr分別介紹:
3.1.2 型別type
型別常量 | 物件名稱 | TYPE命令輸出 |
---|---|---|
REDIS_STRING |
字串物件string | "string" |
REDIS_LIST |
列表物件list | "list" |
REDIS_HASH |
雜湊物件hash(map) | "hash" |
REDIS_SET |
集合物件set | "set" |
REDIS_ZSET |
有序集合物件stored-set | "zset" |
注意,看這個表,一定要區分好“型別常量”、“物件名稱”,如下:
1)當我們稱呼一個資料庫鍵為“字串鍵”時,我們指的是“這個資料庫鍵對應的值為字串物件”;當我們稱呼一個資料庫鍵為“列表鍵”時,我們指的是“這個資料庫鍵對應的值為列表物件”;
2)TYPE命令輸出(上表第三列):當我們對一個資料庫鍵執行TYPE命令時,命令返回的結果是資料庫鍵對應的值物件的型別,而不是鍵物件的型別。
其實,這些東西都是一些概念理論上的糾結,實際開發中,我們以實現需求為主,也不一定要區分的這麼清楚,當然面試中可能用得到。
3.1.3 編碼encoding
編碼常量 | 編碼對應的底層資料結構 | redis中具體型別(5種) |
OBJECT ENCODING 命令輸出 |
REDIS_ENCODING_INT | long型別整數(編碼常量字尾是INT,但是其實現的底層資料結構是long) | REDIS_STRING | "int" |
REDIS_ENCODING_EMBSTR | embstr編碼的簡單動態字串(SDS simple dynamic string) | REDIS_STRING | "embstr" |
REDIS_ENCODING_RAW | 簡單動態字串(SDS simple dynamic string) | REDIS_STRING | "raw" |
REDIS_ENCODING_HT | 字典(編碼常量字尾為HT,表示dictionary/hashtable,即字典) | REDIS_HASH、REDIS_SET | "hashtable" |
REDIS_ENCODING_LINKEDLIST | 雙向連結串列/雙端連結串列(linkedlist,見名達意,不解釋) | REDIS_LIST | "linkedlist" |
REDIS_ENCODING_ZIPLIST | 壓縮列表(ziplist,見名達意,不解釋) |
REDIS_LIST、 REDIS_HASH、REDIS_ZSET |
"ziplist" |
REDIS_ENCODING_INTSET | 整型集合(intset,見名達意,不解釋) | REDIS_SET | "intset" |
REDIS_ENCODING_SKIPLIST | 跳躍表和字典(skiplist,見名達意,不解釋) | REDIS_ZSET | "skiplist" |
3.1.4 sds(這個很重要,下面會用到)
sds英文全稱 simple dynamic string,這裡是簡單動態字串
struct sdshdr{
// 記錄buf陣列中未使用位元組的數量
int free;
// 記錄buf陣列中已使用位元組的數量,等於sds所儲存字串的長度
int len;
// 位元組陣列,用於儲存字串
char buff[];
}
結構(下面介紹五種基本型別底層結構會用到):
free:0 表示這個sds沒有分配任何未使用空間;
len:5 表示這個sds儲存了一個5個位元組的字串;
buf:Hello 表示一個char型別的陣列,陣列的前五個位元組分別儲存了‘H’‘e’‘l’‘l’‘o’,最後一個字元儲存了空字元‘\0’.
3.2 字串物件string
由上面的編碼表可以知道,字串編碼包括三種 int raw embstr,三者對比:
如果儲存的是整數值,且可用long型別表示,那麼編碼設為int;
如果儲存的是一個字串,並且長度大於32位元組,那麼使用SDS(simple dynamic string,簡單動態字串)儲存,編碼設為raw;
如果儲存的是一個字串,並且長度小於或等於32位元組,那麼編碼設為embstr;
3.2.1 int編碼
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為string
encoding,redis任何一種基本型別至少有兩種編碼,這裡是int
ptr:指標,指向底層資料結構的指標,這裡指向底層long型別資料7758258
3.2.2 raw編碼
對於這個圖的解釋:
redisObject: 分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為string
encoding,redis任何一種基本型別至少有兩種編碼,這裡是raw
ptr:指標,指向底層資料結構的指標,這裡指向sdshdr
sdshdr :
sds simple dynamic string,簡單動態字串 ; hdr High available Data Replication,高可用性複製 ; 合在一起 sds hdr 簡單動態字串高可用性複製,包括三個 free 表示
free:0 表示這個sds沒有分配任何未使用空間;
len:36表示這個sds儲存了一個36個位元組的字串;
buf:Hello 表示一個char型別的陣列,陣列的前36個位元組分別儲存了'H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’'H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’'H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’'.’‘.’‘.’,
最後一個字元儲存了空字元‘\0’.
3.2.3 embstr編碼
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為string
encoding,redis任何一種基本型別至少有兩種編碼,這裡是embstr
ptr:指標,指向底層資料結構的指標,這裡指向sdshdr
sdshdr :
sds simple dynamic string,簡單動態字串
hdr High available Data Replication,高可用性複製
合在一起 sds hdr 簡單動態字串高可用性複製,包括三個 free 表示
free:0 表示這個sds沒有分配任何未使用空間;
len:5 表示這個sds儲存了一個5個位元組的字串;
buf:Hello 表示一個char型別的陣列,陣列的前五個位元組分別儲存了‘H’‘e’‘l’‘l’‘o’,最後一個字元儲存了空字元‘\0’.
最後點一下,用long double型別表示的浮點數在redis中也是字串來表示的,瞭解即可。
3.3 列表物件list
由上面的編碼表可以知道,字串編碼包括兩種:linkedlist ziplist
3.3.1 ziplist編碼
zlbytes:表示的是總長度,總位元組數
zllen:表示的是資料部分的長度
兩個不一樣的。
3.3.2 linkedlist編碼
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為list
encoding,redis任何一種基本型別至少有兩種編碼,這裡是linkedlist
ptr:指標,指向底層資料結構的指標1 表示ziplist第一個元素
“three” 表示ziplist第二個元素
5 表示ziplist第三個元素
3.4 雜湊物件hash(map)
由上面的編碼表可以知道,字串編碼包括兩種:ziplist hashtable
3.4.1 ziplist編碼
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為list
encoding,redis任何一種基本型別至少有兩種編碼,這裡是ziplist
ptr:指標,指向底層資料結構的指標
zlbytes: 表示整個ziplist的位元組數(總長度)
zltail: 表示整個ziplist的頭部
zllen: 表示整個ziplist 資料部分的長度
(key,value)=(name,Tom) 表示ziplist第一個元素
(key,value)=(age,25) 表示ziplist第二個元素
(key,value)=(career,Programmer) 表示ziplist第三個元素
zlled 表示整個ziplist的尾部
3.4.2 hashtable編碼
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為list
encoding,redis任何一種基本型別至少有兩種編碼,這裡是hashtable
ptr:指標,指向底層資料結構的指標
3.5 集合物件set
由上面的編碼表可以知道,字串編碼包括兩種:intset hashtable
3.5.1 intset編碼
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為set
encoding,redis任何一種基本型別至少有兩種編碼,這裡是intset
ptr:指標,指向底層資料結構的指標
3.5.2 hashtable編碼
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為set
encoding,redis任何一種基本型別至少有兩種編碼,這裡是hashtable
ptr:指標,指向底層資料結構的指標
3.6 有序集合物件sortedset
由上面的編碼表可以知道,字串編碼包括兩種:ziplist skiplist
3.6.1 ziplist編碼
ziplist編碼的有序集合物件使用壓縮列表作為底層實現。每個集合使用2個緊挨在一起的壓縮列表節點來儲存,第一個儲存元素的成員,第二個儲存元素的分值。壓縮列表內的集合按分值從小到大排序,分值較小的元素被放置在靠近表頭的位置,分值較大的元素在靠近表尾的位置。
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為sorted set
encoding,redis任何一種基本型別至少有兩種編碼,這裡是ziplist
ptr:指標,指向底層資料結構的指標
zlbytes: 表示整個ziplist的位元組數(總長度)
zltail: 表示整個ziplist的頭部
zllen: 表示整個ziplist 資料部分的長度
(key,value)=(apple,8.5) 表示ziplist第一個元素
(key,value)=(banana,5.0) 表示ziplist第二個元素
(key,value)=(cherry,6.0) 表示ziplist第三個元素
zlled 表示整個ziplist的尾部
3.6.2 skiplist編碼
skiplist編碼的有序集合物件使用 zset結構作為底層實現,zset結構同時包含一個字典和一個跳躍表。如下:
typedef struct zset{
dict *dict; // 字典dict
zskiplist *zsl; // 跳躍表zsl
}zset; //zset是有序集合,同時由字典dict和跳躍表zsl實現
為什麼有序集合zset(sorted set)要同時由字典dict和跳躍表實現?
跳躍表利於執行範圍操作(跳躍表是排好序的),而字典有利於執行分值查詢操作。同時由於Redis裡的跳躍表和字典元素很多都是用指標實現的,所以不會浪費記憶體。
對於這個圖的解釋:
redisObject:
分為三個 型別type 編碼encoding 指標ptr
type為五種基本型別,這裡為sorted set
encoding,redis任何一種基本型別至少有兩種編碼,這裡是skiplist
ptr:指標,指向底層資料結構的指標zset 表示sorted set 實體
dict 表示字典
zsl 表示sorted skiplist 跳躍表
…… 表示跳躍
(key,value)=(apple,8.5) 表示ziplist第一個元素
(key,value)=(banana,5.0) 表示ziplist第二個元素
(key,value)=(cherry,6.0) 表示ziplist第三個元素
3.7 小結(五種基本型別的底層結構)
對於redis的五種基本型別(string hash list set sortedset),是redis最基本的知識,本文第三部分介紹這五種基本型別的底層實現,給讀者一個進一步理解的空間。
四、單機版redis及Java開發實踐
4.1 單機版Redis安裝
步驟一:安裝相關依賴。
yum install gcc-c++
步驟二:官網下載redis原始碼包、解壓
官網下載redis原始碼包
解壓:tar -zxvf redis-3.0.0.tar.gz
步驟三:編譯和安裝
解壓後,進入解壓目錄,編譯安裝
[root@bogon redis-3.0.0]# make
[root@bogon redis-3.0.0]# make install PREFIX=/usr/local/redis (要安裝的目錄 PREFIX一定要大寫)
步驟四:啟動(進入剛剛安裝位置啟動)
cd /usr/local/redis/bin
./redis-server (啟動)
成功標誌:
看到這個圖片為啟動成功。
附:redis兩種啟動方式 (前端啟動+後端啟動)
1、預設啟動方式為前端啟動
cd /usr/local/redis/bin/ (這裡指進入redis安裝目錄,bin目錄下)
./redis-server (啟動)
預設是前端啟動模式,埠是6379
2、後端啟動
(1)從redis的解壓目錄中複製redis.conf到redis的安裝目錄(本文中/usr/local/redis)
(2)修改安裝目錄下剛剛複製過來的配置檔案:
daemonize yes (表示使用後端模式啟動)
(3)[root@bogon bin]# ./redis-server redis.conf 啟動
單機版redis安裝完成。
步驟五:測試使用(centos上測試使用)
127.0.0.1:6379> set a 10
OK
127.0.0.1:6379> get a
"10"
可以看到,redis可以正常使用。
4.2 Java專案中使用redis
4.2.1 Junit測試使用Jedis單連線redis
匯入依賴
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.0</version>
</dependency>
test使用
// Junit測試連線redis和基本的set-get操作
@Test
public void testJedisSingle() {
Jedis jedis = new Jedis("192.168.101.3", 6379);
//這裡表示centos IP為192.168.101.3
jedis.set("name", "bar");
String name = jedis.get("name");
System.out.println(name);
jedis.close();
}
附:如果centos上的redis正常啟動而且測試可以完成set-get基本操作,但是本地無法連線上centos上的redis,檢視防火牆是否關閉。正確的操作是關閉防火牆或開啟6379號埠。centos7.0防火牆操作如下:
使用命令:systemctl status firewalld.service檢視防火牆狀態‘’
使用命令:systemctl stop firewalld.service 關閉防火牆
使用命令:systemctl start firewalld.service 開啟防火牆
新增
firewall-cmd --zone=public --add-port=6379/tcp --permanent (--permanent永久生效,沒有此引數重啟後失效)
重新載入
firewall-cmd --reload
檢視
firewall-cmd --zone= public --query-port=6379/tcp注:關閉防火牆和開啟6379號埠只要兩者選一即可。
4.2.2 進階——Junit測試使用連線池連線redis
通過單例項連線redis不能對redis連線進行共享,可以使用連線池對redis連線進行共享,提高資源利用率,使用jedisPool連線redis服務,如下程式碼:
@Test
public void pool() {
JedisPoolConfig config = new JedisPoolConfig();
//最大連線數
config.setMaxTotal(30);
//最大連線空閒數
config.setMaxIdle(2);
JedisPool pool = new JedisPool(config, "192.168.101.3", 6379);
Jedis jedis = null;
try {
jedis = pool.getResource();
jedis.set("name", "lisi");
String name = jedis.get("name");
System.out.println(name);
}catch(Exception ex){
ex.printStackTrace();
}finally{
if(jedis != null){
//關閉連線
jedis.close();
}
}
}
4.2.3 再次進階——在spring中使用redis
上面的都是使用Junit的測試程式碼,不是專案中真正執行有效程式碼,這裡介紹spring中整合redis
配置spring配置檔案applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.2.xsd ">
<!-- 連線池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大連線數 -->
<property name="maxTotal" value="30" />
<!-- 最大空閒連線數 -->
<property name="maxIdle" value="10" />
<!-- 每次釋放連線的最大數目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 釋放連線的掃描間隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 連線最小空閒時間 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 連線空閒多久後釋放, 當空閒時間>該值 且 空閒連線>最大空閒連線數 時直接釋放 -->
<property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 獲取連線時的最大等待毫秒數,小於零:阻塞不確定的時間,預設-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在獲取連線的時候檢查有效性, 預設false -->
<property name="testOnBorrow" value="true" />
<!-- 在空閒時檢查有效性, 預設false -->
<property name="testWhileIdle" value="true" />
<!-- 連線耗盡時是否阻塞, false報異常,ture阻塞直到超時, 預設true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis單機 通過連線池 -->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="close">
<constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
<constructor-arg name="host" value="192.168.101.3"/>
<constructor-arg name="port" value="6379"/>
</bean>
測試程式碼:
private ApplicationContext applicationContext;
@Before
public void init() {
applicationContext = new ClassPathXmlApplicationContext(
"classpath:applicationContext.xml");
}
@Test
public void testJedisPool() {
JedisPool pool = (JedisPool) applicationContext.getBean("jedisPool");
try {
jedis = pool.getResource();
jedis.set("name", "lisi");
String name = jedis.get("name");
System.out.println(name);
}catch(Exception ex){
ex.printStackTrace();
}finally{
if(jedis != null){
//關閉連線
jedis.close();
}
}
}
4.4 小結(redis單機版)
redis單機版使用起來的並不難,下載對應版本,在centos上安裝好,設定好防火牆和埠,即可使用jedis連線使用。
五、叢集版redis及Java開發實踐
5.1 redis叢集原理
5.1.1 Redis叢集架構圖
redis叢集架構細節:
(1)所有的redis節點彼此互聯(PING-PONG機制),內部使用二進位制協議優化傳輸速度和頻寬.
(2)節點的fail是通過叢集中超過半數的節點檢測失效時才生效.
(3)客戶端與redis節點直連,不需要中間proxy層.客戶端不需要連線叢集所有節點,連線叢集中任何一個可用節點即可
(4)redis-cluster把所有的物理節點對映到[0-16383]slot上,cluster 負責維護node<->slot<->value
Redis 叢集中內建了 16384 個雜湊槽,當需要在 Redis 叢集中放置一個 key-value 時,redis 先對 key 使用 crc16 演算法算出一個結果,然後把結果對 16384 求餘數,這樣每個 key 都會對應一個編號在 0-16383 之間的雜湊槽,redis 會根據節點數量大致均等的將雜湊槽對映到不同的節點
5.1.2 redis-cluster投票:容錯
redis叢集的容錯機制——相關問題:
(1)領著投票過程是叢集中所有master參與,如果半數以上master節點與master節點通訊超過(cluster-node-timeout),認為當前master節點掛掉.
(2):什麼時候整個叢集不可用(cluster_state:fail)?
a:如果叢集任意master掛掉,且當前master沒有slave.叢集進入fail狀態,也可以理解成叢集的slot對映[0-16383]不完成時進入fail狀態. ps : redis-3.0.0.rc1加入cluster-require-full-coverage引數,預設關閉,開啟叢集相容部分失敗.
b:如果叢集超過半數以上master掛掉,無論是否有slave叢集進入fail狀態.
ps:當叢集不可用時,所有對叢集的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)錯誤
5.2 叢集版redis(redis-cluster)安裝
5.2.1 手把手搭建ruby環境
redis叢集管理工具redis-trib.rb依賴ruby環境,故首先需要在centos上安裝ruby環境:
安裝ruby環境:
yum install ruby
yum install rubygems
安裝好ruby環境後,安裝ruby和redis的介面程式:
拷貝redis-3.0.0.gem至/usr/local下
執行:
gem install /usr/local/redis-3.0.0.gem
5.2.2 新建叢集所需結點(6個)
這裡在同一臺伺服器用不同的埠表示不同的redis伺服器,如下:
主節點:192.168.101.3:7001 192.168.101.3:7002 192.168.101.3:7003
從節點:192.168.101.3:7004 192.168.101.3:7005 192.168.101.3:7006
在/usr/local下建立redis-cluster目錄,其下建立7001、7002。。7006目錄,命令如下:
cd /usr/local (進入/usr/local目錄)
mkdir redis-cluster (建立redis-cluster目錄)
cd redis-cluster (進入redis-cluster目錄)
mkdir 7001 7002 7003 7004 7005 7006 (建立7001 7002 7003 7004 7005 7006目錄)
執行結果如下:
將redis安裝目錄bin下的檔案拷貝到每個700X(一共7001~7006 6個)目錄內,同時將redis原始碼目錄src下的redis-trib.rb拷貝到redis-cluster目錄下。
修改每個700X目錄下的redis.conf配置檔案:
port XXXX (指定redis埠,但是記住,6個redis的埠不同,避免埠衝突)
#bind 192.168.101.3
cluster-enabled yes (允許構成叢集)
5.2.3 啟動所有redis結點(6個)
這裡使用後端啟動,分別進入7001、7002、...7006目錄,執行:
./redis-server ./redis.conf
然後檢視6個是否都已經成功啟動:
ps aux|grep redis
執行結果,如下則為6個都啟動成功(7001~7006)(PS:ps aux|grep 程式名,檢視程式啟動情況,可以在任意目錄下執行此句,不一定要在7006目錄下)
5.2.4 將6個redis結點整合
執行redis-trib.rb,此指令碼是ruby指令碼,它依賴ruby環境。
./redis-trib.rb create --replicas 1 192.168.101.3:7001 192.168.101.3:7002 192.168.101.3:7003 192.168.101.3:7004 192.168.101.3:7005 192.168.101.3:7006
(嗯,你沒有想錯,6個redis建立一個redis叢集只需要這僅僅一條命令)
輸出(建立叢集輸出)如下:
>>> Creating cluster
Connecting to node 192.168.101.3:7001: OK
Connecting to node 192.168.101.3:7002: OK
Connecting to node 192.168.101.3:7003: OK
Connecting to node 192.168.101.3:7004: OK
Connecting to node 192.168.101.3:7005: OK
Connecting to node 192.168.101.3:7006: OK
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
192.168.101.3:7001
192.168.101.3:7002
192.168.101.3:7003
Adding replica 192.168.101.3:7004 to 192.168.101.3:7001
Adding replica 192.168.101.3:7005 to 192.168.101.3:7002
Adding replica 192.168.101.3:7006 to 192.168.101.3:7003
M: cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 192.168.101.3:7001
slots:0-5460 (5461 slots) master
M: 4e7c2b02f0c4f4cfe306d6ad13e0cfee90bf5841 192.168.101.3:7002
slots:5461-10922 (5462 slots) master
M: 1a8420896c3ff60b70c716e8480de8e50749ee65 192.168.101.3:7003
slots:10923-16383 (5461 slots) master
S: 69d94b4963fd94f315fba2b9f12fae1278184fe8 192.168.101.3:7004
replicates cad9f7413ec6842c971dbcc2c48b4ca959eb5db4
S: d2421a820cc23e17a01b597866fd0f750b698ac5 192.168.101.3:7005
replicates 4e7c2b02f0c4f4cfe306d6ad13e0cfee90bf5841
S: 444e7bedbdfa40714ee55cd3086b8f0d5511fe54 192.168.101.3:7006
replicates 1a8420896c3ff60b70c716e8480de8e50749ee65
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join...
>>> Performing Cluster Check (using node 192.168.101.3:7001)
M: cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 192.168.101.3:7001
slots:0-5460 (5461 slots) master
M: 4e7c2b02f0c4f4cfe306d6ad13e0cfee90bf5841 192.168.101.3:7002
slots:5461-10922 (5462 slots) master
M: 1a8420896c3ff60b70c716e8480de8e50749ee65 192.168.101.3:7003
slots:10923-16383 (5461 slots) master
M: 69d94b4963fd94f315fba2b9f12fae1278184fe8 192.168.101.3:7004
slots: (0 slots) master
replicates cad9f7413ec6842c971dbcc2c48b4ca959eb5db4
M: d2421a820cc23e17a01b597866fd0f750b698ac5 192.168.101.3:7005
slots: (0 slots) master
replicates 4e7c2b02f0c4f4cfe306d6ad13e0cfee90bf5841
M: 444e7bedbdfa40714ee55cd3086b8f0d5511fe54 192.168.101.3:7006
slots: (0 slots) master
replicates 1a8420896c3ff60b70c716e8480de8e50749ee65
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
附加說明1:語句中replicas指定為1表示每個主節點有一個從節點。實際上,任何一個redis叢集至少需要3個主節點,這裡因為一共6個結點,拿出3個主節點後,所以3個從節點,每一個主節點有一個從節點。
附加說明2:
若執行時報如下錯誤:
[ERR] Node XXXXXX is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0
解決方法是刪除生成的配置檔案nodes.conf,如果不行則說明現在建立的結點包括了舊叢集的結點資訊,需要刪除redis的持久化檔案後再重啟redis,比如:appendonly.aof、dump.rdb
5.2.5 查詢叢集資訊(結點資訊+狀態資訊)
步驟一:叢集建立成功登陸任意redis結點查詢叢集中的節點情況。
客戶端以叢集方式登陸:./redis-cli -c -h 192.168.101.3 -p 7001
如下:
說明:
./redis-cli -c -h 192.168.101.3 -p 7001 ,其中-c表示以叢集方式連線redis,-h指定ip地址,-p指定埠號
登入之後,查詢結點資訊和狀態資訊:
cluster nodes 查詢叢集結點資訊
cluster info 查詢叢集狀態資訊
5.3 Java使用redis叢集、Spring容器使用redis叢集(jedisCluster)
5.3.1 Java使用Junit測試方法
// 連線redis叢集
@Test
public void testJedisCluster() {
JedisPoolConfig config = new JedisPoolConfig();
// 最大連線數
config.setMaxTotal(30);
// 最大連線空閒數
config.setMaxIdle(2);
//叢集結點
Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7001));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7002));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7003));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7004));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7005));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7006));
JedisCluster jc = new JedisCluster(jedisClusterNode, config);
JedisCluster jcd = new JedisCluster(jedisClusterNode);
jcd.set("name", "zhangsan");
String value = jcd.get("name");
System.out.println(value);
}
5.3.2 Spring使用redis叢集
程式碼1——Spring配置檔案applicationContext.xml配置:
<!-- 連線池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大連線數 -->
<property name="maxTotal" value="30" />
<!-- 最大空閒連線數 -->
<property name="maxIdle" value="10" />
<!-- 每次釋放連線的最大數目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 釋放連線的掃描間隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 連線最小空閒時間 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 連線空閒多久後釋放, 當空閒時間>該值 且 空閒連線>最大空閒連線數 時直接釋放 -->
<property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 獲取連線時的最大等待毫秒數,小於零:阻塞不確定的時間,預設-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在獲取連線的時候檢查有效性, 預設false -->
<property name="testOnBorrow" value="true" />
<!-- 在空閒時檢查有效性, 預設false -->
<property name="testWhileIdle" value="true" />
<!-- 連線耗盡時是否阻塞, false報異常,ture阻塞直到超時, 預設true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis叢集 -->
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
<constructor-arg index="0">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7001"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7002"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7003"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7004"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7005"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7006"></constructor-arg>
</bean>
</set>
</constructor-arg>
<constructor-arg index="1" ref="jedisPoolConfig"></constructor-arg>
</bean>
程式碼2——spring容器測試redis叢集
private ApplicationContext applicationContext;
@Before
public void init() {
applicationContext = new ClassPathXmlApplicationContext(
"classpath:applicationContext.xml");
}
//redis叢集
@Test
public void testJedisCluster() {
JedisCluster jedisCluster = (JedisCluster) applicationContext
.getBean("jedisCluster");
jedisCluster.set("name", "zhangsan");
String value = jedisCluster.get("name");
System.out.println(value);
}
5.4 附加部分:關於centos上面的redis叢集節點操作(包括:新增主節點、新增從節點、刪除節點)
5.4.1 新增主節點
叢集建立成功後可以向叢集中新增節點,下面是新增一個master主節點
新增7007結點
執行下邊命令:
./redis-trib.rb add-node 192.168.101.3:7007 192.168.101.3:7001
進入redis結點,檢視叢集結點發現7007已新增到叢集中:
centosIP:redis埠號>cluster nodes
新增完主節點需要對主節點進行hash槽分配這樣該主節才可以儲存資料。
redis叢集有16384個槽,叢集中的每個結點分配自已槽,通過檢視叢集結點可以看到槽佔用情況。
給剛新增的7007結點分配槽:
第一步:連線上叢集
./redis-trib.rb reshard 192.168.101.3:7001(連線叢集中任意一個可用結點都行)
第二步:輸入要分配的槽數量
輸入 500表示要分配500個槽
第三步:輸入接收槽的結點id
這裡準備給7007分配槽,通過cluster nodes檢視7007結點id為15b809eadae88955e36bcdbb8144f61bbbaf38fb
輸入:15b809eadae88955e36bcdbb8144f61bbbaf38fb
第四步:輸入源結點id
這裡輸入all
第五步:輸入yes開始移動槽到目標結點id
5.4.2 新增從節點
叢集建立成功後可以向叢集中新增節點,下面是新增一個slave從節點。
新增7008從結點,將7008作為7007的從結點。
./redis-trib.rb add-node --slave --master-id 主節點id 新增節點的ip和埠 叢集中已存在節點ip和埠
執行如下命令:
./redis-trib.rb add-node --slave --master-id cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 192.168.101.3:7008 192.168.101.3:7001
cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 是7007結點的id,可通過cluster nodes檢視。
注意:如果原來該結點在叢集中的配置資訊已經生成cluster-config-file指定的配置檔案中(如果cluster-config-file沒有指定則預設為nodes.conf),這時可能會報錯:
[ERR] Node XXXXXX is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0
解決方法是刪除生成的配置檔案nodes.conf,刪除後再執行./redis-trib.rb add-node指令
檢視叢集中的結點,剛新增的7008為7007的從節點:
5.4.3 刪除節點
./redis-trib.rb del-node 127.0.0.1:7005 4b45eb75c8b428fbd77ab979b85080146a9bc017
刪除已經佔有hash槽的結點會失敗,報錯如下:
[ERR] Node 127.0.0.1:7005 is not empty! Reshard data away and try again.
需要將該結點佔用的hash槽分配出去。
5.5 小結(redis叢集)
本部分介紹redis叢集的底層原理、redis叢集安裝、redis叢集使用、附加部分,幫助讀者充分理解redis-cluster原理及使用。
六、小結
本文主要介紹四個知識,包括redis基礎知識、redis底層原理、單機版redis、叢集版redis(redis-cluster),從基礎知識、底層原理到開發實踐,幫助讀者全面學習redis。
天天打碼,天天進步!
相關文章
- 敏捷軟體開發:原則,模式,實踐敏捷模式
- 從程式棧記憶體底層原理到Segmentation fault報錯記憶體Segmentation
- Redis中介軟體與Web中介軟體RedisWeb
- Redis Pipelining 底層原理分析及實踐Redis
- 從原理到實戰,徹底搞懂NginxNginx
- 從-1開始實現一箇中介軟體
- 【中介軟體】Redis 實戰之主從複製、高可用、分散式Redis分散式
- 軟體開發最佳實踐
- 用JS開發跨平臺桌面應用,從原理到實踐JS
- 中介軟體redis的使用Redis
- 孔子=?中介軟體開發框架?框架
- 敏捷軟體開發:原則、模式與實踐讀書摘要敏捷模式
- 軟體開發和產品經理到底是怎麼回事
- 開源資料庫中介軟體-MyCa初探與分片實踐資料庫
- 快速軟體開發最佳實踐(2)
- 快速軟體開發最佳實踐(1)
- 軟體開發和測試的 30 個最佳實踐
- 從原理到實戰,徹底搞懂Nginx(高階篇)Nginx
- 徹底搞懂Scrapy的中介軟體(三)
- 徹底搞懂Scrapy的中介軟體(二)
- 徹底搞懂Scrapy的中介軟體(一)
- 《敏捷軟體開發 原則、模式與實踐》的讀書筆記敏捷模式筆記
- C#中的訊息中介軟體(RabbitMQ 和 Redis)C#MQRedis
- SpringBoot分散式任務中介軟體開發 附視訊講解 (手把手教你開發和使用中介軟體)Spring Boot分散式
- 從通訊開始聊聊訊息中介軟體
- N層結構與中介軟體(zt)
- Redis的底層實現---字串章節Redis字串
- 訊息中介軟體Kafka+Zookeeper叢集簡介、部署和實踐Kafka
- Decorator:從原理到實踐,我一點都不虛~
- 讀《深入分散式快取 - 從原理到實踐》分散式快取
- DDoS 攻擊與防禦:從原理到實踐
- iOS開發UIScrollView的底層實現iOSUIView
- 重新整理 .net core 實踐篇—————中介軟體[十九]
- 分庫分表中介軟體的高可用實踐
- vivo 超大規模訊息中介軟體實踐之路
- 訊息中介軟體Notify和MetaQ-阿里中介軟體阿里
- 《閒扯Redis十一》Redis 有序集合物件底層實現Redis物件
- 【軟體開發底層知識修煉】六 Binutils輔助工具之- addr2line與strip工具