Redis是使用C編寫的,內部實現了一個struct結構體redisObject對象,經過結構體來模仿面向對象編程的「多態」,動態支持不一樣類型的value。做爲一個底層的數據支持,redisObject結構體代碼以下定義:java
#define LRU_BITS 24 #define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */ #define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */ typedef struct redisObject { //對象的數據類型,佔4bits,共5種類型 unsigned type:4; //對象的編碼類型,佔4bits,共10種類型 unsigned encoding:4; //least recently used //實用LRU算法計算相對server.lruclock的LRU時間 unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ //引用計數 int refcount; //指向底層數據實現的指針 void *ptr; } robj;
下面介紹type、encoding、ptr3個屬性定義枚舉。
type:redisObject的類型,字符串、列表、集合、有序集、哈希表linux
//type的佔5種類型: /* Object types */ #define OBJ_STRING 0 //字符串對象 #define OBJ_LIST 1 //列表對象 #define OBJ_SET 2 //集合對象 #define OBJ_ZSET 3 //有序集合對象 #define OBJ_HASH 4 //哈希對象
encoding:底層實現結構,字符串、整數、跳躍表、壓縮列表等web
/* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */ // encoding 的10種類型 #define OBJ_ENCODING_RAW 0 /* Raw representation */ //原始表示方式,字符串對象是簡單動態字符串 #define OBJ_ENCODING_INT 1 /* Encoded as integer */ //long類型的整數 #define OBJ_ENCODING_HT 2 /* Encoded as hash table */ //字典 #define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ //不在使用 #define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */ //雙端鏈表,不在使用 #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ //壓縮列表 #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ //整數集合 #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ //跳躍表和字典 #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ //embstr編碼的簡單動態字符串 #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */ //由壓縮列表組成的雙向列表-->快速列表
ptr:實際指向保存值的數據結構
若是一個 redisObject 的 type 屬性爲 OBJ_LIST,encoding 屬性爲 REDIS_ENCODING_LINKEDLIST,那麼這個對象就是一個 Redis 列表,它的值保存在一個雙鏈表內,而 ptr 指針就指向這個雙向鏈表;若是一個 redisObject 的type屬性爲OBJ_HASH,encoding 屬性REDIS_ENCODING_ZIPMAP,那麼這個對象就是一個 Redis 哈希表,它的值保存在一個 zipmap 裏,而 ptr 指針就指向這個 zipmap 。
下面這張圖片中的OBJ_STRING/OBJ_LIST/OBJ_ZSET/OBJ_HASH/OBJ_SET針對的是redisObject中的type,後面指向的REDIS_ENCODING_INT、REDIS_ENCODING_RAW、REDIS_ENCODING_LINKEDLIST等針對的是encoding字段。redis
redis結構與編碼組合列表算法
Redis的底層數據結構有如下幾種,具體的數據結構原理就不細講了:shell
字符串對象的底層實現類型以下:數據庫
編碼—encoding | 對象—ptr |
---|---|
OBJ_ENCODING_RAW | 簡單動態字符串實現的字符串對象 |
OBJ_ENCODING_INT | 整數值實現的字符串對象 |
OBJ_ENCODING_EMBSTR | embstr編碼的簡單動態字符串實現的字符串對象 |
若是一個String類型的value可以保存爲整數,則將對應redisObject 對象的encoding修改成REDIS_ENCODING_INT,將對應redisObject對象的ptr值改成對應的數值;若是不能轉爲整數,保持原有encoding爲REDIS_ENCODING_RAW。所以String類型的數據可能使用原始的字符串存儲(實際爲sds - Simple Dynamic Strings,對應encoding爲REDIS_ENCODING_RAW或OBJ_ENCODING_EMBSTR)或者整數存儲。
字符串編碼存在OBJ_ENCODING_RAW和OBJ_ENCODING_EMBSTR兩種,redis會根據value中字符串的大小動態選擇。建立一個String類型的redis值,分配空間的代碼以下:編程
RedisObj *o = zmalloc(sizeof(RedisObj)+sizeof(struct sdshdr8)+len+1);
其中:sdshdr8(保存字符串對象的結構)的大小爲3個字節,加上1個結束符共4個字節;redisObject的大小爲16個字節;一個embstr固定的大小爲16+3+1 = 20個字節,所以一個最大的embstr字符串爲64-20 = 44字節。建立字符串對象,根據長度使用不一樣的編碼類型--createRawStringObject或createEmbeddedStringObject。當字符串長度大於44字節時,使用createRawStringObject,此時redisobj結構和sdshdr結構(存儲具體字符串內容)在內存上是分開的;當字符串長度小於等於44字節時,使用createEmbeddedStringObject,此時redisObj結構和sdshdr結構在內存上是連續的。數組
列表的底層實現有2種:REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST,ZIPLIST(壓縮列表)相比LINKEDLIST(連接列表)能夠節省內存,當建立新的列表時,默認是使用壓縮列表做爲底層數據結構的。Redis內部會對相關操做作判斷,當list的元數小於配置值: hash-max-ziplist-entries 或者elem_value字符串的長度小於 hash-max-ziplist-value, 能夠編碼成 REDIS_ENCODING_ZIPLIST 類型存儲,以節約內存。
壓縮列表ziplist結構自己就是一個連續的內存塊,由表頭、若干個entry節點和壓縮列表尾部標識符zlend組成,經過一系列編碼規則,提升內存的利用率,使用於存儲整數和短字符串。
壓縮列表是一系列特殊編碼的連續內存塊組成的順序序列數據結構,能夠包含任意多個節點(entry),每個節點能夠保存一個字節數組或者一個整數值。
壓縮列表數據實現的指針指向的結構以下圖所示:緩存
壓縮列表數據結構
壓縮列表ziplist結構的缺點是:每次插入或刪除一個元素時,都須要進行頻繁的調用realloc()函數進行內存的擴展或減少,而後進行數據」搬移」,甚至可能引起連鎖更新,形成嚴重效率的損失。
建立新的Hash類型時,默認也使用ziplist存儲value,保存數據過多時,使用hash table。
redisObject對象中存放的是結構體dict,定義以下:
typedefstruct dict { dictType *type; //指向dictType結構,dictType結構中包含自定義的函數,這些函數使得key和value可以存儲任何類型的數據。 void *privdata; //私有數據,保存着dictType結構中函數的參數。 dictht ht[2]; //兩張哈希表。用於擴展或收縮 long rehashidx; //rehash的標記,rehashidx==-1,表示沒在進行rehash int iterators; //正在迭代的迭代器數量 } dict;
其中dictht(Redis中哈希表)定義以下:
typedefstruct dictht { //哈希表 dictEntry **table; //數組地址,數組存放着哈希表節點dictEntry的地址。 unsignedlong size; //哈希表table的大小,初始化大小爲4 unsignedlong sizemask; //值老是等於(size-1)。 unsignedlong used; //記錄哈希表已有的節點(鍵值對)數量。 } dictht;
其中dictEntry就是存放key和value的結構體。
總體的結構以下:
hash類型的redis值結構
集合的底層實現也有兩種:REDIS_ENCODING_INTSET和REDIS_ENCODING_HT(字典),建立Set類型的key-value時,若是value可以表示爲整數,則使用intset類型保存value。不然切換爲使用hash table保存各個value(hash table,參考上面Hash的介紹),雖然使用散列表對集合的加入刪除元素,判斷元素是否存在等操做時間複雜度爲O(1),可是當存儲的元素是整型且元素數目較少時,若是使用散列表存儲,就會比較浪費內存,所以整數集合(intset)類型由於節約內存而存在。
整數集合(intset)結構體定義以下:
typedefstruct intset { uint32_t encoding; //編碼格式,有以下三種格式,初始值默認爲INTSET_ENC_INT16 uint32_t length; //集合元素數量 int8_t contents[]; //保存元素的數組,元素類型並不必定是ini8_t類型,柔性數組不佔intset結構體大小,而且數組中的元素從小到大排列。 } intset; //整數集合結構
整數集合(intset)類型的編碼格式有下面三種:
#define INTSET_ENC_INT16 (sizeof(int16_t)) //16位,2個字節,表示範圍-32,768~32,767 #define INTSET_ENC_INT32 (sizeof(int32_t)) //32位,4個字節,表示範圍-2,147,483,648~2,147,483,647 #define INTSET_ENC_INT64 (sizeof(int64_t)) //64位,8個字節,表示範圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807
intset整數集合之因此有三種表示編碼格式的宏定義,是由於根據存儲的元素數值大小,可以選取一個最」合適」的類型存儲,」合適」能夠理解爲:既可以表示元素的大小,又能夠節省空間。所以,當新添加的元素,例如:65535,超過當前集合編碼格式所能表示的範圍,就要進行升級操做。
有序集合的底層編碼實現也是2種:REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_SKIPLIST。跳躍表在redis中當數據較多時做爲有序集合鍵的實現方式之一。跳躍表是一個有序鏈表,其中每一個節點包含不定數量的連接,節點中的第i個連接構成的單向鏈表跳過含有少於i個連接的節點。
跳躍表支持平均O(logN),最壞O(N)複雜度的節點查找,大部分狀況下,跳躍表的效率能夠和平衡樹相媲美。
咱們知道redis與memcached的一個很大的不一樣是redis能夠將數據持久化到磁盤,能持久化意味着數據的可靠性的提高。
RDB(redis database)是一個磁盤存儲的數據庫文件,其中保存的是最後一次寫入時內存數據的最後狀態。因爲Redis的數據都存放在內存中,若是沒有配置持久化,redis重啓後數據就全丟失了,因而須要開啓redis的持久化功能,將數據保存到磁盤上,當redis重啓後,能夠從磁盤中恢復數據。redis提供兩種方式進行持久化,一種是RDB持久化(原理是將Reids在內存中的數據庫記錄定時dump到磁盤上的RDB持久化,這稱爲「半持久化模式」),另一種是AOF(append only file)持久化(原理是將Reids的操做日誌以追加的方式寫入文件,這稱爲「全持久化模式」)。
RDB的持久化方式經過配置的定時執行間隔定時將內存中的數據寫入到一個新的臨時RDB文件中,而後用這個臨時文件替換上次持久化的RDB文件,如此不斷的定時更替。
當redis server重啓時,會檢查當前配置的持久化方式,若是是AOF(Append Of File)則以AOF數據做爲恢復數據,由於AOF備份的準確性每每比RDB更高。若是是隻開啓了RDB模式的話則會加載最新的RDB文件內容到內存中。
另外redis也提供了手動調用的命令來實施RDB備份,包括阻塞的持久化和非阻塞的持久化。
RDB持久化
RDB持久化是指在指定的時間間隔內將內存中的數據集快照寫入磁盤,實際操做過程是fork一個子進程,先將數據集寫入臨時文件,寫入成功後,再替換以前的文件,用二進制壓縮存儲。
RDB持久化的時間間隔能夠配置,和該配置項一塊兒配合使用的還有另外一個指標「變動次數」,每次時間間隔時必須同時符合「間隔時間」和「變動次數」兩個條件纔會進行RDB持久化,不然當次的持久化過程會推遲到下一個時間間隔再判斷是否符合條件。
AOF持久化
當Redis開啓AOF持久化時,每次接收到操做指令後,先將操做命令和數據以格式化的方式追加到操做日誌文件的尾部,追加成功後才進行內存數據庫的數據變動。這樣操做日誌文件就保存了全部的歷史操做過程。該過程與MySQL的bin.log、zookeeper的txn-log十分類似。
AOF保存的是每次操做的操做序列,相比較而言RDB保存的是數據快照,所以AOF的操做日誌文件內容每每比RDB文件大。
須要注意的是,由於linux對文件的寫操做採起了「延遲寫入」手段,所以redis提供了always、everysec、no三種選擇來決定直接調用操做系統文件寫入的刷盤動做。
AOF先記錄後變動的特性決定了數據的可靠性更高,所以當AOF和RDB持久化都配置時,Redis服務在重啓後會優先選擇AOF數據做爲數據恢復標準。
執行AOF數據恢復時,Redis讀取AOF文件中的「操做+數據」集,經過逐條重放的方式恢復內存數據庫。
AOF文件會不斷增大,它的大小直接影響「故障恢復」的時間,並且AOF文件中歷史操做是能夠丟棄的。AOF rewrite操做就是「壓縮」AOF文件的過程,固然redis並無採用「基於原aof文件」來重寫的方式,而是採起了相似snapshot的方式:基於copy-on-write,全量遍歷內存中數據,而後逐個序列到aof文件中。所以AOF rewrite可以正確反應當前內存數據的狀態,這正是咱們所須要的。rewrite過程當中,對於新的變動操做將仍然被寫入到原AOF文件中,同時這些新的變動操做也會被redis收集起來(buffer,copy-on-write方式下,最極端的多是全部的key都在此期間被修改,將會耗費2倍內存),當內存數據被所有寫入到新的aof文件以後,收集的新的變動操做也將會一併追加到新的aof文件中,此後將會重命名新的aof文件爲appendonly.aof,此後全部的操做都將被寫入新的aof文件。若是在rewrite過程當中,出現故障,將不會影響原AOF文件的正常工做,只有當rewrite完成以後纔會切換文件,由於rewrite過程是比較可靠的。
Redis事務一般會使用MULTI,EXEC,WATCH等命令來完成,redis實現事務的機制與常見的關係型數據庫有很大的卻別,好比redis的事務不支持回滾,事務執行時會阻塞其它客戶端的請求執行等。
MULTI
用於標記事務塊的開始。Redis會將後續的命令逐個放入隊列中,每個指令的返回結果都是「QUEUED」。只有先執行MULTI指令後才能使用EXEC命令原子化地執行這個命令序列。老是返回OK。
EXEC
在一個事務中執行全部先前放入隊列的命令,而後恢復正常的鏈接狀態。EXEC指令的返回值是隊列中多條指令的有序結果。
當在事務中使用了WATCH命令監控的KEY時,只有當受監控的鍵沒有被修改時,EXEC命令纔會執行事務中的隊列命令集合。
DISCARD
清除全部先前在一個事務中放入隊列的命令,而後恢復正常的鏈接狀態。
若是使用了WATCH命令,那麼DISCARD命令就會將當前鏈接監控的全部鍵取消監控。
WATCH
watch 用於在進行事務操做的最後一步也就是在執行exec 以前對某個key進行監視,若是這個被監視的key被改動,那麼事務就被取消,不然事務正常執行。通常在MULTI 命令前就用watch命令對某個key進行監控。若是當前鏈接監控的key值被其它鏈接的客戶端修改,那麼當前鏈接的EXEC命令將執行失敗。
WATCH命令的做用只是當被監控的鍵值被修改後阻止事務的執行,而不能保證其餘客戶端不修改這一鍵值。
UNWATCH
清除全部先前爲一個事務監控的鍵。執行EXEC命令後會取消對全部鍵的監控,若是不想執行事務中的命令也可使用UNWATCH命令來取消監控。UNWATCH命令,清除全部受監控的鍵。在運行UNWATCH命令以後,Redis鏈接即可以再次自由地用於運行新事務。
redis事務從開始到結束一般會經過三個階段:
1)事務開始
2)命令入隊
3)事務執行
標記事務的開始,MULTI命令能夠將執行該命令的客戶端從非事務狀態切換成事務狀態,這一切換是經過在客戶端狀態的flags屬性中打開REDIS_MULTI標識完成, 在打開事務標識的客戶端裏,這些命令,都會被暫存到一個命令隊列裏,不會由於用戶的輸入而當即執行。客戶端打開了事務標識後,只有命令: EXEC, DISCARD, WATCH,MULTI命令會被當即執行,其它命令服務器不會當即執行,而是將這些命令放入到一個事務隊列裏面,而後向客戶端返回一個QUEUED回覆 。redis客戶端有本身的事務狀態,這個狀態保存在客戶端狀態mstate屬性中。
在redis中事務老是具備原子性(Atomicity),一致性(Consistency)和隔離性(Isolation),而且當redis運行在某種特定的持久化模式下,事務也具備持久性(Durability)。
原子性
事務具備原子性指的是事務中的多個操做看成一個總體來執行,服務器要麼就執行事務中的全部操做,要麼就一個操做也不執行。可是對於redis的事務功能來講,事務隊列中的命令要麼就所有執行,要麼就一個都不執行,所以redis的事務是具備原子性的(有條件的原子性)。咱們一般會知道兩種關於redis事務原子性的說法:一種是要麼事務都執行,要麼都不執行;另一種說法是redis事務,當事務中的命令執行失敗後面的命令還會執行,錯誤以前的命令不會回滾。其實這個兩個說法都是正確的,redis分語法錯誤和運行錯誤。
只有當被調用的Redis命令有語法錯誤時,這條命令纔會執行失敗(在將這個命令放入事務隊列期間,Redis可以發現此類問題),或者對某個鍵執行不符合其數據類型的操做:實際上,這就意味着只有程序錯誤纔會致使Redis命令執行失敗,這種錯誤頗有可能在程序開發期間發現,通常不多在生產環境發現。
Redis已經在系統內部進行功能簡化,這樣能夠確保更快的運行速度,由於Redis不須要事務回滾的能力。
一致性
事務具備一致性指的是若是在執行事務以前是一致的,那麼在事務執行以後,不管事務是否執行成功,數據庫也應該仍然一致的。 「一致」指的是數據符合數據庫自己的定義和要求,沒有包含非法或者無效的錯誤數據。redis經過謹慎的錯誤檢測和簡單的設計來保證事務一致性。若是遇到運行錯誤,redis的原子性也不能保證,因此一致性也是有條件的一致性。
隔離性
事務的隔離性指的是即便有多個事務併發在執行,各個事務之間也不會互相影響,而且在併發狀態下執行的事務和串行執行的事務產生的結果徹底相同。 由於redis使用單線程的方式來執行事務(以及事務隊列中的命令),而且服務器保證,在執行事務期間不會對事物進行中斷。所以redis的事務老是以串行的方式運行的,而且事務也老是具備隔離性的 。
持久性
事務的持久性指的是當一個事務執行完畢時,執行這個事務所得的結果已經被保持到永久存儲介質裏面。 由於redis事務不過是簡單的用隊列包裹起來一組redis命令,redis並無爲事務提供任何額外的持久化功能,因此redis事務的持久性由redis使用的模式決定 :
經過EXPIRE key seconds命令來設置數據的過時時間。返回1代表設置成功,返回0代表key不存在或者不能成功設置過時時間。key的過時信息以絕對Unix時間戳的形式存儲(Redis2.6以後以毫秒級別的精度存儲)。這意味着,即便Redis實例沒有運行也不會對key的過時時間形成影響。
key被DEL命令刪除或者被SET、GETSET命令重置後與之關聯的過時時間會被清除。
更新了存儲在key中的值而沒有用全新的值替換key原有值的全部操做都不會影響在該key上設置的過時時間。例如使用INCR命令增長key的值或者經過LPUSH命令在list中增長一個新的元素或者使用HSET命令更新hash字段的值都不會清除原有的過時時間設置。
若key被RENAME命令重寫,好比本存在名爲mykey_a和mykey_b的key一個RENAME mykey_b mykey_a命令將mykey_b重命名爲本已存在的mykey_a。那麼不管mykey_a原來的設置如何都將繼承mykey_b的全部特性,包括過時時間設置。
EXPIRE key seconds應用於一個已經設置了過時時間的key上時原有的過時時間將被更新爲新的過時時間。
當clients試圖訪問設置了過時時間且已過時的key時,這個時候將key刪除再返回空,爲主動過時方式。但僅是這樣是不夠的,由於可能存在一些key永遠不會被再次訪問到,這些設置了過時時間的key也是須要在過時後被刪除的。所以,Redis會週期性的隨機測試一批設置了過時時間的key並進行處理。測試到的已過時的key將被刪除,這種爲被動過時方式。典型的方式爲,Redis每秒作10次以下的步驟:
1)隨機測試100個設置了過時時間的key
2)刪除全部發現的已過時的key
3)若刪除的key超過25個則重複步驟1
這是一個基於機率的簡單算法,基本的假設是抽出的樣本可以表明整個key空間,redis持續清理過時的數據直至將要過時的key的百分比降到了25%如下。這也意味着在任何給定的時刻已通過期但仍佔據着內存空間的key的量最多爲每秒的寫操做量除以4。
redis 3.0版本開始提供的集羣服務,服務端實現的集羣。Redis Cluster將全部Key映射到16384個Slot中,集羣中每一個Redis實例負責一部分,實例之間雙向通訊。業務程序經過集成的Redis Cluster客戶端進行操做。客戶端能夠向任一實例發出請求,若是所需數據不在該實例中,則該實例引導客戶端自動去對應實例讀寫數據。
redis啓動以後,用戶必須開啓集羣模式,經過cluster-enabled yes 設置。經過執行cluster meet 命令來完成鏈接各個redis單例服務,redis 節點必須進行槽(slot)指派,這樣就創建一個redis 集羣了。沒有槽指派,集羣是不能正常運用起來.
redis 集羣是經過分片方式來存儲鍵值的,集羣默認將整個redis 數據庫分紅16384個槽(slot),每一個節點必須作槽指派。不然集羣處於fail 狀態。經過shell命令來指派槽,必須把16384槽都分配到不一樣節點。
此種方式集羣在添加和刪除節點時,需經過手動腳本命令進行添加和刪除,槽必須須要從新分配。這種集羣不能自動發現節點,節點的健康情況,缺少管理頁面監控整個集羣的情況。
redis 3.0以前版本的集羣方式,是客戶端實現集羣的方案。創建由N個節點組成一個集羣,各redis節點相互獨立,不會進行相互通訊。客戶端預先設置的路由規則,直接對多個Redis實例進行分佈式訪問。
採用一致性hash算法(將key和節點name同時hashing)將redis 數據散列對應的節點,這樣客戶端就知道從哪一個Redis節點獲取數據。當增長或減小節點時,不會產生因爲從新匹配形成的rehashing。
客戶端實現的集羣缺點:
經過中間代理層實現的集羣方案以codis最爲經典,codis的結構圖以下:
codis結構圖
這裏以codis爲例分析,codis-proxy 是Redis客戶端鏈接的代理服務,客戶端經過鏈接codis-proxy,codis-proxy指定鏈接後面具體的redis實例。Redis客戶端經過zk上的註冊信息來得到當前可用的proxy列表,從而保證代理的高可用性。
咱們爲何選用codis方案做爲redis的集羣方案,緣由以下:
場景一:顯示最新的列表; 使用功能:Redis中的列表
在Web應用中,「列出最新的回覆」之類的查詢很是廣泛,這一般會帶來可擴展性問題。相似的問題就能夠用Redis來解決。好比說,咱們的一個Web應用想要列出用戶貼出的最新20條評論。在最新的評論邊上咱們有一個「顯示所有」的連接,點擊後就能夠得到更多的評論。
咱們假設數據庫中的每條評論都有一個惟一的遞增的ID字段。咱們可使用分頁來製做主頁和評論頁,使用Redis的模板:
1)每次新評論發表時,咱們會將它的ID添加到一個Redis列表:
LPUSH latest.comments <ID>
2)咱們將列表裁剪爲指定長度,所以Redis只須要保存最新的5000條評論:
LTRIM latest.comments 05000
3)每次咱們須要獲取最新評論的項目範圍時,咱們調用一個函數來完成(使用僞代碼):
FUNCTION get_latest_comments(start,num_items):
id_list = redis.lrange("latest.comments",start,start+num_items-1)
IF id_list.length < num_items
id_list = SQL_DB("SELECT ... ORDER BY time LIMIT ...")
END
RETURN id_list
END
咱們作了限制不能超過5000個ID,所以咱們的獲取ID函數會一直詢問Redis。只有在start/count參數超出了這個範圍的時候,才須要去訪問數據庫。
咱們的系統不會像傳統方式那樣「刷新」緩存,Redis實例中的信息永遠是一致的。SQL數據庫(或是硬盤上的其餘類型數據庫)只是在用戶須要獲取「很遠」的數據時纔會被觸發,而主頁或第一個評論頁是不會麻煩到硬盤上的數據庫了。
場景二:刪除與過濾; 使用功能: Redis中的集合
好比郵箱的垃圾郵件功能,包含特定詞或者來自特定發送方。
有些時候你想要給不一樣的列表附加上不一樣的過濾器。若是過濾器的數量有限,你能夠簡單的爲每一個不一樣的過濾器使用不一樣的Redis列表。
場景三:根據某個屬性進行排名之類; 使用功能:Redis的有序集合
另外一個很廣泛的需求是各類數據庫的數據並不是存儲在內存中,所以在大數據量場景下按得分排序,數據庫的性能不夠理想。
典型的好比那些在線遊戲的排行榜,根據得分你一般想要:
若是用數據庫的的order by排序,這種相應時間很是長,沒法支持大併發請求。可是這些操做對於Redis來講小菜一碟,即便你有幾百萬個用戶,每分鐘都會有幾百萬個新的得分。
向有序集合添加一個或多個成員,或者更新已存在成員的分數:
ZADD key score1 member1 [score2 member2]
獲得前100名高分用戶很簡單:
ZREVRANGE key 0 99
用戶的全球排名也類似,只須要:
ZRANK key
場景四:過時項目處理 ; 使用功能:Redis的有序集合和過時時間
另外一種經常使用的項目排序是按照時間排序。而且只須要保留必定時間內的數據。
這時咱們可使用current_time(unix時間)做爲得分,用Redis的有序集合來存儲。並同時經過expire設置time_to_live。
場景五:計數; 使用功能:Redis的原子操做
Redis是一個很好的計數器,這要感謝INCRBY和其餘類似命令。能夠用於分佈式場景下的全局計數器。我相信你曾許屢次想要給數據庫加上新的計數器,用來獲取統計或顯示新信息,可是最後卻因爲寫入敏感而不得不放棄它們。如今使用Redis就不須要再擔憂了。有了原子遞增(atomic increment),你能夠放心的加上各類計數,用GETSET重置,或者是讓它們過時。
**場景六:特定時間內的特定項目; 使用功能:redis的有序集合 **
另外一項對於其餘數據庫很難,但Redis作起來卻垂手可得的事就是統計在某段特色時間裏有多少特定用戶訪問了某個特定資源。好比我想要知道某些特定的註冊用戶或IP地址,他們到底有多少訪問了某篇文章。
每次得到一次新的頁面瀏覽時只須要這樣作:
SADD page:day1:<page_id>:<user_id>
固然你可能想用unix時間替換day1,好比time()-(time()%3600*24)等等。
想知道特定用戶的數量嗎?只須要使用
SCARD page:day1:<page_id>
須要測試某個特定用戶是否訪問了這個頁面
SISMEMBER page:day1:<page_id>
場景七: Pub/Sub; 使用功能:經過watch命令
Redis的Pub/Sub很是很是簡單,運行穩定而且快速。支持模式匹配,可以實時訂閱與取消頻道。你應該已經注意到像list push和list pop這樣的Redis命令可以很方便的執行隊列操做了,但能作的可不止這些:好比Redis還有list pop的變體命令,可以在列表爲空時阻塞隊列。
場景八:分佈式同步、分佈式鎖; 使用功能:鎖(訪問同一個key實現)
從redis獲取值N,對數值N進行邊界檢查,自加1,而後N寫回redis中。 這種應用場景很常見,像秒殺,全局遞增ID、IP訪問限制等。以IP訪問限制來講,惡意攻擊者可能發起無限次訪問,併發量比較大,分佈式環境下對N的邊界檢查就不可靠,由於從redis讀的N可能已是髒數據。傳統的加鎖的作法(如java的synchronized和Lock)也沒用,由於這是分佈式環境,這種場景就須要分佈式鎖。
分佈式鎖能夠基於不少種方式實現,無論哪一種方式,他的基本原理是不變的:用一個狀態值表示鎖,對鎖的佔用和釋放經過狀態值來標識。
Redis爲單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,且多客戶端對Redis的鏈接並不存在競爭關係。redis的SETNX命令能夠方便的實現分佈式鎖,設置成功,返回 1 ,不然返回 0 。
上面的鎖定邏輯有一個問題:若是一個持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎麼解決?咱們能夠經過鎖的鍵對應的時間戳來判斷這種狀況是否發生了,若是當前的時間已經大於鎖對應的值,說明該鎖已失效,能夠被從新使用。
發生這種狀況時,不能簡單的經過DEL來刪除鎖,而後再SETNX一次,當多個客戶端檢測到鎖超時後都會嘗試去釋放它,這裏就可能出現一個競態條件。 爲了讓分佈式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖以前應該再檢查一次本身的鎖是否已經超時,再去作DEL操做(不是DEL,而是getset命令),這個時候可能已經被其餘線程先set值了,經過比較值錢get的值和getset返回的值是否相等,能夠判別當前線程是否得到鎖。
更多關於分佈式鎖的實現,請參考Java分佈式鎖三種實現方案
緩存穿透是指查詢一個不存在的數據,致使這個不存在的數據每次請求都要到存儲層去查詢。在流量大時,可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊咱們的應用,這就是漏洞。
緩存擊穿問題通常出如今某個高併發訪問的key忽然到了過時時間。緩存擊穿和緩存雪崩的區別在於前者針對某一key緩存,後者則是不少key。
有人會說不設置過時時間,就不會出現熱點key過時問題,也就是「物理」不過時,這樣就不存在緩存擊穿的問題。從功能上看,若是緩存數據不過時,那就成靜態的數據了,理論上也就不須要redis這種緩存。
緩存使用能夠從實時性和是否熱點兩個維度來選擇解決緩存擊穿方案。
實時性
你們都知道,緩存是從數據中同步過來的,因此它是有延遲的。業務對延遲的容忍度,就是實時性要求。實時性是業務要求,是一個沒法妥協的變量。有的延遲能夠在秒級別,有的能到分鐘級別,有的就是零容忍。不一樣的實時性要求,就能夠採起不一樣的緩存同步策略。
熱點
熱點值指的的一個資源(注意不是頁面)被同時訪問的用戶數,這個值比較高時纔是熱點。熱點是技術要求,由於緩存擊穿問題基本都是熱點引發的,因此在設計緩存方案的時候必需要考慮熱點。舉例:商品詳情頁面,假設這個頁面的併發量是1w,但其中最大的商品的併發量卻可能很低,假設只有50併發。咱們認爲這個頁面不存在熱點。這裏的熱點,指的是資源熱點,或者說數據熱點。
image.png
處理方案(綠色方框)
1)懶加載
懶加載
先從緩存中取,若是沒有則從數據庫中取,再放入緩存。
特色:維護成本低、實時性差,命中率低(遇到熱點,可能出現數據庫擊穿的問題)
2)推送
推送
經過獨立的任務,週期性的將數據刷入緩存。這裏除了任務以外,也多是一個消息觸發。
特色:維護成本適中,實時性適中(週期性任務),命中率100%
推送的方案一般能夠結合任務中間件或消息中間件(公司能夠考慮個人另外一篇文章DRC實戰),他們具備更大的靈活性。干預度強,也能夠實現降級。
3)懶加載:二級緩存
二級緩存
送數據庫獲取數據後,放入一個短時間緩存和一個長期緩存。在短時間緩存過時後,經過加鎖控制去數據庫加載數據的線程數。沒有得到鎖的,直接從二級緩存獲取數據。
特色:維護成本適中,實時性適中,命中率100%,該方案能夠解決動態熱點。是推送方案的補充
4)雙寫
雙寫
一邊寫入數據庫,一邊寫入緩存
特色:維護成本最高(侵入代碼),實時性高,命中率100%
使用優先級:1)>2)>3)>4)。響應的維護成本越低越優先
做者:彥幀 連接:https://www.jianshu.com/p/85c713d07895 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。