redis的基本使用方式是創建在redis提供的數據結構上的。react
字符串 REDIS_STRING (字符串)是 Redis 使用得最爲普遍的數據類型,它除了是 SET 、GET 等命令 的操做對象以外,數據庫中的全部鍵,以及執行命令時提供給 Redis 的參數,都是用這種類型 保存的。redis
字符串類型分別使用 REDIS_ENCODING_INT 和 REDIS_ENCODING_RAW 兩種編碼數據庫
只有能表示爲 long 類型的值,纔會以整數的形式保存,其餘類型 的整數、小數和字符串,都是用 sdshdr 結構來保存編程
哈希表 REDIS_HASH (哈希表)是HSET 、HLEN 等命令的操做對象數組
它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_HT 兩種編碼方式緩存
Redis 中每一個hash能夠存儲232-1鍵值對(40多億)安全
列表 REDIS_LIST(列表)是LPUSH 、LRANGE等命令的操做對象服務器
它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST 這兩種方式編碼網絡
一個列表最多能夠包含232-1 個元素(4294967295, 每一個列表超過40億個元素)。數據結構
集合 REDIS_SET (集合) 是 SADD 、 SRANDMEMBER 等命令的操做對象
它使用 REDIS_ENCODING_INTSET 和 REDIS_ENCODING_HT 兩種方式編碼
Redis 中集合是經過哈希表實現的,因此添加,刪除,查找的複雜度都是O(1)。
集合中最大的成員數爲 232 - 1 (4294967295, 每一個集合可存儲40多億個成員)
有序集 REDIS_ZSET (有序集)是ZADD 、ZCOUNT 等命令的操做對象
它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_SKIPLIST 兩種方式編碼
不一樣的是每一個元素都會關聯一個double類型的分數。redis正是經過分數來爲集合中的成員進行從小到大的排序。
有序集合的成員是惟一的,但分數(score)卻能夠重複。
集合是經過哈希表實現的,因此添加,刪除,查找的複雜度都是O(1)。 集合中最大的成員數爲 232 - 1 (4294967295, 每一個集合可存儲40多億個成員)
下圖說明了,外部數據結構和底層實際數據結構是經過realobject來鏈接的。一個外觀類型裏面必然存着一個realobject,經過它來訪問底層數據結構。
下面討論redis底層數據結構
1 SDS動態字符串
sds字符串是字符串的實現
動態字符串是一個結構體,內部有一個buf數組,以及字符串長度,剩餘長度等字段,優勢是經過長度限制寫入,避免緩衝區溢出,另外剩餘長度不足時會自動擴容,擴展性較好,不須要頻繁分配內存。
而且sds支持寫入二進制數據,而不必定是字符。
2 dict字典
dict字典是哈希表的實現。
dict字典與Java中的哈希表實現簡直一模一樣,首先都是數組+鏈表組成的結構,經過dictentry保存節點。
其中dict同時保存兩個entry數組,當須要擴容時,把節點轉移到第二個數組便可,平時只使用一個數組。
3 壓縮鏈表ziplist
3.1 ziplist是一個通過特殊編碼的雙向鏈表,它的設計目標就是爲了提升存儲效率。ziplist能夠用於存儲字符串或整數,其中整數是按真正的二進制表示進行編碼的,而不是編碼成字符串序列。它能以O(1)的時間複雜度在表的兩端提供push和pop操做。
3.2 實際上,ziplist充分體現了Redis對於存儲效率的追求。一個普通的雙向鏈表,鏈表中每一項都佔用獨立的一塊內存,各項之間用地址指針(或引用)鏈接起來。這種方式會帶來大量的內存碎片,並且地址指針也會佔用額外的內存。
3.3 而ziplist倒是將表中每一項存放在先後連續的地址空間內,一個ziplist總體佔用一大塊內存。它是一個表(list),但其實不是一個鏈表(linked list)。
3.4 另外,ziplist爲了在細節上節省內存,對於值的存儲採用了變長的編碼方式,大概意思是說,對於大的整數,就多用一些字節來存儲,而對於小的整數,就少用一些字節來存儲。
實際上。redis的字典一開始的數據比較少時,會使用ziplist的方式來存儲,也就是key1,value1,key2,value2這樣的順序存儲,對於小數據量來講,這樣存儲既省空間,查詢的效率也不低。
當數據量超過閾值時,哈希表自動膨脹爲以前咱們討論的dict。
4 quicklist
quicklist是結合ziplist存儲優點和鏈表靈活性與一身的雙端鏈表。
quicklist的結構爲何這樣設計呢?總結起來,大概又是一個空間和時間的折中:
4.1 雙向鏈表便於在表的兩端進行push和pop操做,可是它的內存開銷比較大。
首先,它在每一個節點上除了要保存數據以外,還要額外保存兩個指針;其次,雙向鏈表的各個節點是單獨的內存塊,地址不連續,節點多了容易產生內存碎片。
4.2 ziplist因爲是一整塊連續內存,因此存儲效率很高。
可是,它不利於修改操做,每次數據變更都會引起一次內存的realloc。特別是當ziplist長度很長的時候,一次realloc可能會致使大批量的數據拷貝,進一步下降性能。
5 zset zset實際上是兩種結構的合併。也就是dict和skiplist結合而成的。dict負責保存數據對分數的映射,而skiplist用於根據分數進行數據的查詢(相輔相成)
6 skiplist
sortset數據結構使用了ziplist+zset兩種數據結構。
Redis裏面使用skiplist是爲了實現sorted set這種對外的數據結構。sorted set提供的操做很是豐富,能夠知足很是多的應用場景。這也意味着,sorted set相對來講實現比較複雜。
sortedset是由skiplist,dict和ziplist組成的。
當數據較少時,sorted set是由一個ziplist來實現的。 當數據多的時候,sorted
set是由一個叫zset的數據結構來實現的,這個zset包含一個dict + 一個skiplist。dict用來查詢數據到分數(score)的對應關係,而skiplist用來根據分數查詢數據(多是範圍查找)。
在本系列前面關於ziplist的文章裏,咱們介紹過,ziplist就是由不少數據項組成的一大塊連續內存。因爲sorted set的每一項元素都由數據和score組成,所以,當使用zadd命令插入一個(數據, score)對的時候,底層在相應的ziplist上就插入兩個數據項:數據在前,score在後。
skiplist的節點中存着節點值和分數。而且跳錶是根據節點的分數進行排序的,因此能夠根據節點分數進行範圍查找。
7inset
inset是一個數字結合,他使用靈活的數據類型來保持數字。
新建立的intset只有一個header,總共8個字節。其中encoding = 2, length = 0。 添加13, 5兩個元素以後,由於它們是比較小的整數,都能使用2個字節表示,因此encoding不變,值仍是2。 當添加32768的時候,它再也不能用2個字節來表示了(2個字節能表達的數據範圍是-215~215-1,而32768等於215,超出範圍了),所以encoding必須升級到INTSET_ENC_INT32(值爲4),即用4個字節表示一個元素。
8總結
sds是一個靈活的字符串數組,而且支持直接存儲二進制數據,同時提供長度和剩餘空間的字段來保證伸縮性和防止溢出。
dict是一個字典結構,實現方式就是Java中的hashmap實現,同時持有兩個節點數組,但只使用其中一個,擴容時換成另一個。
ziplist是一個壓縮鏈表,他放棄內存不連續的鏈接方式,而是直接分配連續內存進行存儲,減小內存碎片。提升利用率,而且也支持存儲二進制數據。
quicklist是ziplist和傳統鏈表的中和造成的鏈表結果,每一個鏈表節點都是一個ziplist。
skiplist通常有ziplist和zset兩種實現方法,根據數據量來決定。zset自己是由skiplist和dict實現的。
inset是一個數字集合,他根據插入元素的數據類型來決定數組元素的長度。並自動進行擴容。
9 他們實現了哪些結構
字符串由sds實現
list由ziplist和quicklist實現
sortset由ziplist和zset實現
hash表由dict實現
集合由inset實現。
1 redis服務器中維護着一個數據庫名爲redisdb,實際上他是一個dict結構。
Redis的數據庫使用字典做爲底層實現,數據庫的增、刪、查、改都是構建在字典的操做之上的。
2 redis服務器將全部數據庫都保存在服務器狀態結構redisServer(redis.h/redisServer)的db數組(應該是一個鏈表)裏:
同理也有一個redis client結構,經過指針能夠選擇redis client訪問的server是哪個。
3 redisdb的鍵空間
typedef struct redisDb {
// 數據庫鍵空間,保存着數據庫中的全部鍵值對
dict *dict; /* The keyspace for this DB */
// 鍵的過時時間,字典的鍵爲鍵,字典的值爲過時事件 UNIX 時間戳
dict *expires; /* Timeout of keys with a timeout set */
// 數據庫號碼
int id; /* Database ID */
// 數據庫的鍵的平均 TTL ,統計信息
long long avg_ttl; /* Average TTL, just for stats */
//..
} redisDb
複製代碼
這部分的代碼說明了,redisdb除了維護一個dict組之外,還須要對應地維護一個expire的字典數組。
大的dict數組中有多個小的dict字典,他們共同負責存儲redisdb的全部鍵值對。
同時,對應的expire字典則負責存儲這些鍵的過時時間
4 過時鍵的刪除策略
二、過時鍵刪除策略 經過前面的介紹,你們應該都知道數據庫鍵的過時時間都保存在過時字典裏,那假如一個鍵過時了,那麼這個過時鍵是何時被刪除的呢?如今來看看redis的過時鍵的刪除策略:
a、定時刪除:在設置鍵的過時時間的同時,建立一個定時器,在定時結束的時候,將該鍵刪除;
b、惰性刪除:聽任鍵過時無論,在訪問該鍵的時候,判斷該鍵的過時時間是否已經到了,若是過時時間已經到了,就執行刪除操做;
c、按期刪除:每隔一段時間,對數據庫中的鍵進行一次遍歷,刪除過時的鍵。
redis處理請求的方式基於reactor線程模型,即一個線程處理鏈接,而且註冊事件到IO多路複用器,複用器觸發事件之後根據不一樣的處理器去執行不一樣的操做。總結如下客戶端到服務端的請求過程
總結
遠程客戶端鏈接到 redis 後,redis服務端會爲遠程客戶端建立一個 redisClient 做爲代理。
redis 會讀取嵌套字中的數據,寫入 querybuf 中。
解析 querybuf 中的命令,記錄到 argc 和 argv 中。
根據 argv[0] 查找對應的 recommand。
執行 recommend 對應的執行函數。
執行之後將結果存入 buf & bufpos & reply 中。
返回給調用方。返回數據的時候,會控制寫入數據量的大小,若是過大會分紅若干次。保證 redis 的相應時間。
Redis 做爲單線程應用,一直貫徹的思想就是,每一個步驟的執行都有一個上限(包括執行時間的上限或者文件尺寸的上限)一旦達到上限,就會記錄下當前的執行進度,下次再執行。保證了 Redis 可以及時響應不發生阻塞。
複製代碼
快照(RDB):就是咱們俗稱的備份,他能夠在按期內對數據進行備份,將Redis服務器中的數據持久化到硬盤中;
只追加文件(AOF):他會在執行寫命令的時候,將執行的寫命令複製到硬盤裏面,後期恢復的時候,只須要從新執行一下這個寫命令就能夠了。相似於咱們的MySQL數據庫在進行主從複製的時候,使用的是binlog二進制文件,一樣的是執行一遍寫命令;
appendfsync同步頻率的區別以下圖:
Redis複製工做過程:
slave向master發送sync命令。
master開啓子進程來說dataset寫入rdb文件,同時將子進程完成以前接收到的寫命令緩存起來。
子進程寫完,父進程得知,開始將RDB文件發送給slave。
master發送完RDB文件,將緩存的命令也發給slave。
master增量的把寫命令發給slave。
注意有兩步操做,一個是寫入rdb的時候要緩存寫命令,防止數據不一致。發完rdb後還要發寫命令給salve,之後增量發命令就能夠了
複製代碼
加鎖時使用setnx設置key爲1並設置超時時間,解鎖時刪除鍵
tryLock(){
SETNX Key 1
EXPIRE Key Seconds
}
release(){
DELETE Key
}
複製代碼
這個方案的一個問題在於每次提交一個Redis請求,若是執行完第一條命令後應用異常或者重啓,鎖將沒法過時,一種改善方案就是使用Lua腳本(包含SETNX和EXPIRE兩條命令),可是若是Redis僅執行了一條命令後crash或者發生主從切換,依然會出現鎖沒有過時時間,最終致使沒法釋放。
針對鎖沒法釋放問題的一個解決方案基於GETSET命令來實現
思路:
SETNX(Key,ExpireTime)獲取鎖
若是獲取鎖失敗,經過GET(Key)返回的時間戳檢查鎖是否已通過期
GETSET(Key,ExpireTime)修改Value爲NewExpireTime
檢查GETSET返回的舊值,若是等於GET返回的值,則認爲獲取鎖成功
注意:這個版本去掉了EXPIRE命令,改成經過Value時間戳值來判斷過時
複製代碼
V2.0 基於SETNX
tryLock(){
SETNX Key 1 Seconds
}
release(){
DELETE Key
}
複製代碼
Redis 2.6.12版本後SETNX增長過時時間參數,這樣就解決了兩條命令沒法保證原子性的問題。可是設想下面一個場景:
流程圖以下
V3.0
tryLock(){
SETNX Key UnixTimestamp Seconds
}
release(){
EVAL(
//LuaScript
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
)
}
複製代碼
這個方案經過指定Value爲時間戳,並在釋放鎖的時候檢查鎖的Value是否爲獲取鎖的Value,避免了V2.0版本中提到的C1釋放了C2持有的鎖的問題;另外在釋放鎖的時候由於涉及到多個Redis操做,而且考慮到Check And Set 模型的併發問題,因此使用Lua腳原本避免併發問題。
若是在併發極高的場景下,好比搶紅包場景,可能存在UnixTimestamp重複問題,另外因爲不能保證分佈式環境下的物理時鐘一致性,也可能存在UnixTimestamp重複問題,只不過極少狀況下會遇到。
redlock的思想就是要求一個節點獲取集羣中N/2 + 1個節點 上的鎖纔算加鎖成功。
不管是基於SETNX版本的Redis單實例分佈式鎖,仍是Redlock分佈式鎖,都是爲了保證下特性
1 主從複製,優勢是備份簡易使用。缺點是不能故障切換,而且不易擴展。
2 使用sentinel哨兵工具監控和實現自動切換。
3 codis集羣方案
首先codis使用代理的方式隱藏底層redis,這樣能夠完美融合之前的代碼,不須要更改redis訪問操做。
而後codis使用了zookeeper進行監控和自動切換。同時使用了redis-group的概念,保證一個group裏是一主多從的主從模型,基於此來進行切換。
4 redis cluster集羣
該集羣是一個p2p方式部署的集羣
Redis cluster是一個去中心化、多實例Redis間進行數據共享的集羣。
每一個節點上都保存着其餘節點的信息,經過任一節點能夠訪問正常工做的節點數據,由於每臺機器上的保留着完整的分片信息,某些機器不正常工做不影響總體集羣的工做。而且每一臺redis主機都會配備slave,經過sentinel自動切換。
事務 MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事務相關的命令。事務能夠一次執行多個命令, 而且帶有如下兩個重要的保證:
事務是一個單獨的隔離操做:事務中的全部命令都會序列化、按順序地執行。事務在執行的過程當中,不會被其餘客戶端發送來的命令請求所打斷。
事務是一個原子操做:事務中的命令要麼所有被執行,要麼所有都不執行。
redis事務有一個特色,那就是在2.6之前,事務的一系列操做,若是有的成功有的失敗,仍然會提交成功的那部分,後來改成所有不提交了。
可是Redis事務不支持回滾,提交之後不能執行回滾操做。
爲何 Redis 不支持回滾(roll back)
若是你有使用關係式數據庫的經驗, 那麼 「Redis 在事務失敗時不進行回滾,而是繼續執行餘下的命令」這種作法可能會讓你以爲有點奇怪。
如下是這種作法的優勢:
Redis 命令只會由於錯誤的語法而失敗(而且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來講,失敗的命令是由編程錯誤形成的,而這些錯誤應該在開發的過程當中被發現,而不該該出如今生產環境中。
由於不須要對回滾進行支持,因此 Redis 的內部能夠保持簡單且快速。
複製代碼
Redis 腳本和事務 從定義上來講, Redis 中的腳本自己就是一種事務, 因此任何在事務裏能夠完成的事, 在腳本里面也能完成。 而且通常來講, 使用腳本要來得更簡單,而且速度更快。
由於腳本功能是 Redis 2.6 才引入的, 而事務功能則更早以前就存在了, 因此 Redis 纔會同時存在兩種處理事務的方法。
redis事務的ACID特性 在傳統的關係型數據庫中,嚐嚐用ACID特質來檢測事務功能的可靠性和安全性。 在redis中事務老是具備原子性(Atomicity),一致性(Consistency)和隔離性(Isolation),而且當redis運行在某種特定的持久化 模式下,事務也具備耐久性(Durability).
①原子性
事務具備原子性指的是,數據庫將事務中的多個操做看成一個總體來執行,服務器要麼就執行事務中的全部操做,要麼就一個操做也不執行。 可是對於redis的事務功能來講,事務隊列中的命令要麼就所有執行,要麼就一個都不執行,所以redis的事務是具備原子性的。
②一致性
事務具備一致性指的是,若是數據庫在執行事務以前是一致的,那麼在事務執行以後,不管事務是否執行成功,數據庫也應該仍然一致的。
」一致「指的是數據符合數據庫自己的定義和要求,沒有包含非法或者無效的錯誤數據。redis經過謹慎的錯誤檢測和簡單的設計來保證事務一致性。
複製代碼
③隔離性
事務的隔離性指的是,即便數據庫中有多個事務併發在執行,各個事務之間也不會互相影響,而且在併發狀態下執行的事務和串行執行的事務產生的結果徹底
相同。
由於redis使用單線程的方式來執行事務(以及事務隊列中的命令),而且服務器保證,在執行事務期間不會對事物進行中斷,所以,redis的事務老是以串行
的方式運行的,而且事務也老是具備隔離性的
複製代碼
④持久性
事務的耐久性指的是,當一個事務執行完畢時,執行這個事務所得的結果已經被保持到永久存儲介質裏面。
由於redis事務不過是簡單的用隊列包裹起來一組redis命令,redis並無爲事務提供任何額外的持久化功能,因此redis事務的耐久性由redis使用的模式
決定複製代碼