本文為筆者對在學習Redis過程中所收集資料的一個總結,目的是為了以后方便回顧相關的知識,大部分為非原創內容。特此聲明!
Redis是什么?
Redis是一個開源的key-value存儲系統,由于擁有豐富的數據結構,又被其作者戲稱為數據結構服務器。它屬于NoSQL(Not Only SQL)數據庫中的鍵值(Key-Value)存儲數據庫,即它屬于與MySQL和Oracle等關系型數據庫不同的非關系型數據庫。它與memcached類似,但是優于memcached。
Redis的應用場景
- 用于做持久化存儲:由于Redis擁有豐富的數據結構,所以可以存儲多種類型的數據。同時,它在存儲與獲取某些數據的效率方面也優于關系型數據庫。例如微博在存儲關注列表和粉絲列表時,就可以使用Redis中的hash sets數據類型用于存儲;在記錄用戶發言數以及粉絲數時,就可以使用Redis中的string(counter)進行存儲,避免了關系型數據庫中的select count(*) from....,減小了系統開銷。但是在實際生產中,一般不會單獨使用Redis作為數據庫。
- 用于數據緩存:Redis最適合所有數據in-momory的場景。
Redis的優點
- 性能很高:Redis支持超過100K每秒的讀寫速率
- 豐富的數據類型(相對于memcached來講):Strings,Lists,Hashes,Sets,Ordered Sets
- 所有操作都是原子性的,不用擔心發生并發問題。并且Redis還支持對幾個操作合并之后的原子性操作
- 擁有其他豐富的特性,例如給Key設置expire過期時間等
Redis對大小寫不敏感
數據類型
Strings
字符串是Redis的一種最基本的數據類型。Redis字符串是二進制安全的,這意味著一個Redis字符串能包含任意類型的數據。一個字符串類型的變量最多能存儲512M字節的內容。
對String常用的操作命令
- set(key, value):給數據庫中名稱為key的string賦予值value
- get(key):返回數據庫中名稱為key的string的value
- getset(key, value):給名稱為key的string賦予上一次的value
- mget(key1, key2,…, key N):返回庫中多個string(它們的名稱為key1,key2…)的value
- setnx(key, value):如果不存在名稱為key的string,則向庫中添加string,名稱為key,值為value
- setex(key, time, value):向庫中添加string(名稱為key,值為value)同時,設定過期時間time
- mset(key1, value1, key2, value2,…key N, value N):同時給多個string賦值,名稱為key i的string賦值value i
- msetnx(key1, value1, key2, value2,…key N, value N):如果所有名稱為key i的string都不存在,則向庫中添加string,名稱key i賦值為value i
- incr(key):名稱為key的string增1操作
- incrby(key, integer):名稱為key的string增加integer
- decr(key):名稱為key的string減1操作
- decrby(key, integer):名稱為key的string減少integer
- append(key, value):名稱為key的string的值附加value
- substr(key, start, end):返回名稱為key的string的value的子串
Lists
Redis中的List是簡單的字符串列表,可以按照插入的順序排序。我們可以添加一個元素到列表的左邊(頭部)或者是右邊(尾部)。對應的命令為LPUSH和RPUSH。
- rpush(key, value):在名稱為key的list尾添加一個值為value的元素
- lpush(key, value):在名稱為key的list頭添加一個值為value的 元素
- llen(key):返回名稱為key的list的長度
- lrange(key, start, end):返回名稱為key的list中start至end之間的元素(下標從0開始,下同)
- ltrim(key, start, end):截取名稱為key的list,保留start至end之間的元素
- lindex(key, index):返回名稱為key的list中index位置的元素
- lset(key, index, value):給名稱為key的list中index位置的元素賦值為value
- lrem(key, count, value):刪除count個名稱為key的list中值為value的元素。count為0,刪除所有值為value的元素,count>0從頭至尾刪除count個值為value的元素,count<0從尾到頭刪除|count|個值為value的元素。 lpop(key):返回并刪除名稱為key的list中的首元素 rpop(key):返回并刪除名稱為key的list中的尾元素 blpop(key1, key2,… key N, timeout):lpop命令的block版本。即當timeout為0時,若遇到名稱為key i的list不存在或該list為空,則命令結束。如果timeout>0,則遇到上述情況時,等待timeout秒,如果問題沒有解決,則對keyi+1開始的list執行pop操作。
- brpop(key1, key2,… key N, timeout):rpop的block版本。參考上一命令。
- rpoplpush(srckey, dstkey):返回并刪除名稱為srckey的list的尾元素,并將該元素添加到名稱為dstkey的list的頭部
Hashes
Hash是字符串字段和字符串值之間的映射,因此他們是展現對象的完美數據類型。一個帶有一些字段的hash僅僅需要一塊很小的空間存儲,因此我們可以存儲數以百萬計的對象在一個小小的redis實例當中。
- hset(key, field, value):向名稱為key的hash中添加元素field<—>value
- hget(key, field):返回名稱為key的hash中field對應的value
- hmget(key, field1, …,field N):返回名稱為key的hash中field i對應的value
- hmset(key, field1, value1,…,field N, value N):向名稱為key的hash中添加元素field i<—>value i
- hincrby(key, field, integer):將名稱為key的hash中field的value增加integer
- hexists(key, field):名稱為key的hash中是否存在鍵為field的域
- hdel(key, field):刪除名稱為key的hash中鍵為field的域
- hlen(key):返回名稱為key的hash中元素個數
- hkeys(key):返回名稱為key的hash中所有鍵
- hvals(key):返回名稱為key的hash中所有鍵對應的value
- hgetall(key):返回名稱為key的hash中所有的鍵(field)及其對應的value
Sets(無序集合)
Redis集合(Set)是一個無序的字符串集合。我們可以在O(1)的時間復雜度(無論集合中有多少元素時間復雜度都是常量)完成添加、刪除或者是查看元素是否存在。Redis集合擁有令人滿意的不允許包含相同成員的屬性。多次添加相同的元素,最終在集合里面只會有一個元素。實際上說這些就是意味著在添加元素的時候無須檢測元素是否存在。一個關于Redis集合非常有趣的事情就是它支持一些服務端的命令從現有的集合出發去進行集合運算,因此我們可以在非常短的時間內合并(unions),求交集(intersections),找出不同的元素(difference of sets)。
- sadd(key, member):向名稱為key的set中添加元素member
- srem(key, member) :刪除名稱為key的set中的元素member
- spop(key) :隨機返回并刪除名稱為key的set中一個元素
- smove(srckey, dstkey, member) :將member元素從名稱為srckey的集合移到名稱為dstkey的集合
- scard(key) :返回名稱為key的set的基數
- sismember(key, member) :測試member是否是名稱為key的set的元素
- sinter(key1, key2,…key N) :求交集
- sinterstore(dstkey, key1, key2,…key N) :求交集并將交集保存到dstkey的集合
- sunion(key1, key2,…key N) :求并集
- sunionstore(dstkey, key1, key2,…key N) :求并集并將并集保存到dstkey的集合
- sdiff(key1, key2,…key N) :求差集
- sdiffstore(dstkey, key1, key2,…key N) :求差集并將差集保存到dstkey的集合
- smembers(key) :返回名稱為key的set的所有元素
- srandmember(key) :隨機返回名稱為key的set的一個元素
Soted Sets(有序集合)
Redis有序集合和普通集合非常類似,是一個沒有重復元素的字符串集合。不同之處在于有序集合的所有成員都關聯了一個評分,這個評分被用來按照從最低分到最高分的方式排序集合中的成員。集合的成員是唯一的,但是評分可以是重復的。使用有序集合我們可以用非常快的速度(O(logN))添加、刪除以及更新元素。因為元素是有序的,所以我們也可以很快地根據評分(score)或者次序(position)來獲取一個范圍的元素。訪問有序集合的中間元素也是非常快的,因此我們能夠使用有序集合作為一個沒有重復成員的智能列表。在有序集合中,我們可以很快捷地訪問一切我們所需要的東西:有序的元素、快速的存在性測試、快速訪問集合的中間元素。簡而言之,使用有序集合我們可以完成許多對性能有極端要求的任務,而這些任務是使用其他類型的數據庫很難完成的。
- zadd(key, score, member):向名稱為key的zset中添加元素member,score用于排序。如果該元素已經存在,則根據score更新該元素的順序。
- zrem(key, member) :刪除名稱為key的zset中的元素member
- zincrby(key, increment, member) :如果在名稱為key的zset中已經存在元素member,則該元素的score增加increment;否則向集合中添加該元素,其score的值為increment
- zrank(key, member) :返回名稱為key的zset(元素已按score從小到大排序)中member元素的rank(即index,從0開始),若沒有member元素,返回“nil”
- zrevrank(key, member) :返回名稱為key的zset(元素已按score從大到小排序)中member元素的rank(即index,從0開始),若沒有member元素,返回“nil”
- zrange(key, start, end):返回名稱為key的zset(元素已按score從小到大排序)中的index從start到end的所有元素
- zrevrange(key, start, end):返回名稱為key的zset(元素已按score從大到小排序)中的index從start到end的所有元素
- zrangebyscore(key, min, max):返回名稱為key的zset中score >= min且score <= max的所有元素 zcard(key):返回名稱為key的zset的基數 zscore(key, element):返回名稱為key的zset中元素element的score zremrangebyrank(key, min, max):刪除名稱為key的zset中rank >= min且rank <= max的所有元素 zremrangebyscore(key, min, max) :刪除名稱為key的zset中score >= min且score <= max的所有元素
- zunionstore / zinterstore(dstkeyN, key1,…,keyN, WEIGHTS w1,…wN, AGGREGATE SUM|MIN|MAX):對N個zset求并集和交集,并將最后的集合保存在dstkeyN中。對于集合中每一個元素的score,在進行AGGREGATE運算前,都要乘以對于的WEIGHT參數。如果沒有提供WEIGHT,默認為1。默認的AGGREGATE是SUM,即結果集合中元素的score是所有集合對應元素進行SUM運算的值,而MIN和MAX是指,結果集合中元素的score是所有集合對應元素中最小值和最大值。
系統管理
- exists key:判斷一個key是否存在。存在返回1,否則返回0
- del key:刪除一個key。成功返回,失敗返回0
- type key:返回key的數據類型:string、list、set、zset、hash。key不存在返回none
- keys key-pattern:將所有能夠匹配key-pattern的key都列出來
- randomkey:隨機返回一個key,如果此時數據庫是空的,則返回一個為空的字符串
- clear:清除界面
- rename oldname newname:將key由原來的oldname改為newname,不管此時newname存不存在
- renamenx oldname newname:將key由原來的oldname改為newname.如果此時newname存在則更改失敗
- dbsize:返回當前數據庫中key的總數
- expire key :限定key的生存時間,命令的一般形式如expire name 30,意思是值為name的key只能存活30秒
- ttl key:返回key剩余存活時間
- flushdb:清除當前數據庫中所有的key
- flushall:清除數據庫中所有的key
- config get:讀取Redis此時的配置參數
- config set:設置Redis此時的配置參數
- auth:密碼認證
- info:可以查詢Redis幾乎所有的信息
底層數據結構
- 簡單動態字符串
- 鏈表
- 字典(Map)
- 跳表
- 整數集合
- 壓縮列表
- 對象
簡單動態字符串(Simple Dynamic String)
雖然Redis是使用C語言編寫的,但是Redis中的字符串類型并不是直接搬用C語言的字符串。
/*字符串對象底層結構*/ struct sds{ int len;//buf已占用的空間長度 int free;//buf中剩余空間長度 char buf[];//數據存儲空間 }
- 獲取字符串長度時間更快(SDS為O(1)/C語言字符串為O(n))
- 避免了緩沖區溢出問題:當我們在對SDS進行修改之前Redis會預先檢查所操作的SDS空間夠不夠。如果不夠,則會拓展對應SDS空間之后再進行拼接等操作。
- 減少修改字符串時帶來的內存分配問題
- 二進制安全
- 兼容部分C語言中有關字符串的函數
鏈表
Redis中的list底層使用的是雙向鏈表
字典(Map)
在字典中,一個key和一個value關聯,并且字典中的每個key都是獨一無二的。
Redis字典使用的哈希表底層結構:
typedef struct dictht { //哈希表數組 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩碼,用于計算索引值 unsigned long sizemask; //該哈希表已有節點的數量 unsigned long used; }
哈希表節點:
typeof struct dictEntry{ //鍵 void *key; //值 union{ void *val; uint64_tu64; int64_ts64; } struct dictEntry *next; }
這一部分主要涉及三個點:
- 根據hash算法算出key的hash值然后分配存儲空間(由于哈希表中沒有記錄鏈表尾節點的位置,所以是在鏈表的head插入新的節點);
- 鏈地址法解決hash地址沖突
- 隨著哈希表中節點數量的增加,適當時候會進行rehash將哈希表的負載因子保持在一個合理的范圍
跳表(skiplist)
跳躍表(skiplist)是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。跳躍表是一種隨機化的數據,跳躍表以有序的方式在層次化的鏈表中保存元素,效率和平衡樹媲美 ——查找、刪除、添加等操作都可以在對數期望時間下完成,并且比起平衡樹來說,跳躍表的實現要簡單直觀得多。Redis 只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另外一個是在集群節點中用作內部數據結構。
只有當有序集合中的元素個數大于128時才會使用跳表,否則將使用后面提到的壓縮列表
- 跳躍表是有序集合的底層實現之一
- 主要有zskiplist 和zskiplistNode兩個結構組成
- 每個跳躍表節點的層高都是1至32之間的隨機數
- 在同一個跳躍表中,多個節點可以包含相同的分值,但每個節點的對象必須是唯一的
- 節點按照分值的大小從大到小排序,如果分值相同,則按成員對象大小排序
zskiplist(鏈表)數據結構:
typedef struct zskiplist { //表頭節點和表尾節點 structz skiplistNode *header,*tail; //表中節點數量 unsigned long length; //表中層數最大的節點的層數 int level; }zskiplist;
zskiplistNode(節點)數據結構:
typedef struct zskiplistNode{ //層 struct zskiplistLevel{ //前進指針 struct zskiplistNode *forward; //跨度 unsigned int span; } level[]; //后退指針 struct zskiplistNode *backward; //分值 double score; //成員對象 robj *obj; }
整數集合(Intset)
整數集合是集合建的底層實現之一,當一個集合中只包含整數,且這個集合中的元素數量不多時,redis就會使用整數集合intset作為集合的底層實現。我們可以這樣理解整數集合,他其實就是一個特殊的集合,里面存儲的數據只能夠是整數,并且數據量不能過大。
整數集合數據結構
typedef struct intset{ //編碼方式 uint32_t enconding; // 集合包含的元素數量 uint32_t length; //保存元素的數組 int8_t contents[]; }
- encoding:用于定義整數集合的編碼方式
- length:用于記錄整數集合中變量的數量
- contents:用于保存元素的數組,雖然我們在數據結構圖中看到,intset將數組定義為int8_t,但實際上數組保存的元素類型取決于encoding
在上述數據結構圖中我們可以看到,intset 在默認情況下會幫我們設定整數集合中的編碼方式,但是當我們存入的整數不符合整數集合中的編碼格式時,就需要使用到Redis 中的升級策略來解決Intset 中升級整數集合并添加新元素共分為三步進行:
- 根據新元素的類型,擴展整數集合底層數組的空間大小,并為新元素分配空間
- 將底層數組現有的所有元素都轉換成新的編碼格式,重新分配空間
- 將新元素加入到底層數組中
整數集合升級不僅可以提高靈活性,還能節約內存。整數集合是集合鍵的底層實現之一。整數集合的底層實現為數組,這個數組以有序,無重復的范式保存集合元素,在有需要時,程序會根據新添加的元素類型改變這個數組的類型。同時,升級操作為整數集合帶來了操作上的靈活性,并且盡可能地節約了內存。但是整數集合只支持升級操作,不支持降級操作。
壓縮列表
壓縮列表是列表鍵和哈希鍵的底層實現之一。當一個列表鍵只含少量列表項(一般是少于128)時并且每個列表項要么就是小整數,要么就是長度比較短的字符串,那么Redis就會使用壓縮列表來做列表鍵的底層實現。
- zlbytes:用于記錄整個壓縮列表占用的內存字節
- zltail:用于記錄列表尾節點距離壓縮列表的起始地址有多少字節
- zllen:用于記錄壓縮列表包含的節點數量
- entry*:列表包含的節點
- zlend:用于標記壓縮列表的末端
關于壓縮列表的幾點總結:
- 壓縮列表是一種為了節約內存而開發的順序型數據結構
- 壓縮列表被用作列表鍵和哈希鍵的底層實現之一
- 壓縮列表可以包含多個節點,每個節點可以保存一個字節數組或者是整數值
- 添加新節點到壓縮列表,可能會引發連鎖更新操作
持久化
Redis直接將數據存儲在內存當中,但是并不是所有的數據都一直存儲在內存中的(這是和memcached相比最大的一個區別)。Redis會緩存所有的key的信息,但是如果Redis發現內存的使用量超過了某一個閾值,就會觸發swap操作。Redis會計算出哪些key對應的value需要swap到磁盤,然后再將這些key對應的value持久化到磁盤中同時清除內存中存儲的對應的value。這種特性使得Redis可以保持超過其機器本身內存大小的數據。但是機器本身的內存必須可以有足夠的空間存儲所有的key,因為key是不會進行swap操作的。由于Redis將內存中的數據swap到磁盤中時,提供服務的主線程和進行swap操作的子線程會共享這部分內存。如果更新需要swap的數據,Redis將阻塞這個操作,直至子線程完成swap操作之后才可以進行修改。
當從Redis中讀取數據的時候,如果讀取的key對應的value不在內存中,那么Redis就要從swap文件加載相應的數據,然后再返回給請求數據的一方。此時存在一個I/O線程池的問題。在默認情況下,Redis會出現阻塞,它要完成所有的swap文件的加載之后才會響應。這樣的策略在客戶端數量較少,進行批量操作的時候比較合適。但是如果將Redis應用在一個大型的網站中,這顯然是無法滿足高并發的需求的。所有Redis允許我們設置I/O線程池的大小,對需要從swap文件中加載對應數據的請求進行并發操作,減少阻塞時間。
Redis提供兩種持久化的機制
-
RDB快照:Redis支持將當前的快照存儲為一個數據文件的持久化機制,即當前提到的RDB快照。Redis借助了fork命令的copy on write機制(私有內存非共享內存)。在生成快照時,將當前進程fork出一個子進程。接著在子進程中迭代循環所有的數據同時將數據存儲到一個臨時文件當中。當數據全部處理完之后,就通過原子性rename系統調用將臨時文件重命名為RDB文件。值得注意的是,這樣的文件生成機制可以保證RDB文件不會壞掉,即Redis的RDB文件總是可用的。但是,RDB有明顯的不足-----一旦數據庫出現問題,那么我們的RDB文件中保存的數據并不是全新的,從上次RDB文件生成到Redis停機這段時間的數據全部丟掉了。在某些業務下,這是可以忍受的,我們也推薦這些業務使用RDB的方式進行持久化,因為開啟RDB的代價并不高。但是對于另外一些對數據安全性要求極高的應用,無法容忍數據丟失的應用,RDB就無能為力了,所以Redis引入了另一個重要的持久化機制:AOF日志。
我們可以通過Redis的save指令來配置快照生成的時機
save 900 1 #900秒內有一條key數據被修改就生成RDB文件
- AOF(Append Only File)日志
AOF日志是追加寫入的日志文件。和一般數據庫的binlog不同的是,AOF文件是可讀性較強的純文本。其中保存的內容即Redis一條條的標準指令。但是并不是所有的Redis指令都會記錄在AOF文件中,只有會導致數據發生修改的指令才會追加到AOF文件中。隨著記錄的指令越來越多,文件會變得越來越大。此時Redis提供了一個叫做AOF rewrite的功能,可以重新生成一份新的并且更小的AOF文件。新的文件中針對同一條key只記錄了最新的一次數據修改的指令。AOF的文件生成機制和RDB快照類似。再寫入新文件的過程中,所有操作日志還是會寫到舊的文件當中,同時會記錄在內存緩沖區中。當rewrite操作完成后,會將所有緩沖區中的日志一次性寫入臨時文件并調用原子性的rename命令將新的AOF文件覆蓋舊的AOF文件。
appendfsync:控制AOF文件寫入磁盤的時機
- appendfsync no:當設置appendfsync為no時,Redis不會主動調用fsync去將AOF日志內容同步到磁盤,這完全依賴于操作系統。對于大多數Linux系統來說,一般是每30秒進行一次fsync,將緩沖區中的數據同步到磁盤上;
- appendfsync everysec:當設置appendfsync為everysec的時候,Redis會默認每隔一秒進行一次fsync調用,將緩沖區中的數據寫到磁盤。但是當這一次的fsync調用時長超過1秒時。Redis會采取延遲fsync的策略,再等一秒鐘。也就是在兩秒后再進行fsync,這一次的fsync就不管會執行多長時間都會進行。這時候由于在fsync時文件描述符會被阻塞,所以當前的寫操作就會阻塞。所以結論就是,在絕大多數情況下,Redis會每隔一秒進行一次fsync。在最壞的情況下,兩秒鐘會進行一次fsync操作。這一操作在大多數數據庫系統中被稱為group commit,就是組合多次寫操作的數據,一次性將日志寫到磁盤。
- appendfsync always:
當設置appendfsync為always時,每一次寫操作都會調用一次fsync,這時數據是最安全的,當然,由于每次都會執行fsync,所以其性能也會受到影響。
主從復制
為了保證單點故障下的數據可用性,Redis引入了Master節點和Slave節點。
- master節點可擁有多個slave節點
- 除了多個slave連到相同的master之外,slave也可以連接其他slave形成圖狀結構
- 主從復制不會阻塞master。也就是說,當一個或多個slave與master進行初次同步數據時,master可以繼續處理client發來的請求。相反slave在初次同步數據時則會阻塞,不能處理client的請求
- 主從復制可以用來提高系統的可伸縮性,我們可以用多個slave專門用于client的讀請求,比如sort操作可以使用slave來處理。也可以用來做簡單的數據冗余
- 可以在master禁用數據持久化,只需要注釋掉master配置文件中的所有save配置,然后只在slave上配置數據持久化
主從復制過程
當設置好slave服務器后,slave會建立和master的連接,接著發送sync命令。無論是第一次同步建立的連接還是連接斷開后的重新連接,master都會啟動一個后臺進程,將數據庫快照保存到文件中,同時master主進程會開始收集新的寫命令并緩存起來。后臺進程完成寫文件后,master就發送文件給slave,slave將文件保存到磁盤上,然后加載到內存并恢復數據庫快照到磁盤上。接著master就會把緩存的命令轉發給slave,而且后續master收到的寫命令都會通過開始建立的連接發送給slave。從master到slave的同步數據的命令和從client發送的命令使用相同的協議格式。當master和slave的連接斷開時slave可以自動重新建立連接。如果master同時收到多個slave發來的同步連接命令,只會啟動一個進程來寫數據庫鏡像,然后發送給所有slave。