https://www.jianshu.com/p/f09480c05e42
1-概覽
Redis是典型的Key-Value類型數據庫,Key為字符類型,Value的類型常用的為五種類型:String、Hash 、List 、 Set 、 Ordered Set
2- Redis內部內存管理
redisObject 核心對象
Redis 內部使用一個 redisObject 對象來表示所有的 key 和 value。
-
type :代表一個 value 對象具體是何種數據類型。
-
encoding :是不同數據類型在 redis 內部的存儲方式,比如:type=string 代表 value 存儲的是一個普通字符串,那么對應的 encoding 可以是 raw 或者是 int,如果是 int 則代表實際 redis 內部是按數值型類存儲和表示這個字符串的,當然前提是這個字符串本身可以用數值表示,比如:"123" "456"這樣的字符串。
-
vm 字段:只有打開了 Redis 的虛擬內存功能,此字段才會真正的分配內存,該功能默認是關閉狀態的。 Redis 使用 redisObject 來表示所有的 key/value 數據是比較浪費內存的,當然這些內存管理成本的付出主要也是為了給 Redis 不同數據類型提供一個統一的管理接口,實際作者也提供了多種方法幫助我們盡量節省內存使用。
3- Key(鍵值)
過期刪除
過期數據的清除從來不容易,為每一條key設置一個timer,到點立刻刪除的消耗太大,每秒遍歷所有數據消耗也大,Redis使用了一種相對務實的做法: 當client主動訪問key會先對key進行超時判斷,過時的key會立刻刪除。 如果clien永遠都不再get那條key呢? 它會在Master的后台,每秒10次的執行如下操作: 隨機選取100個key校驗是否過期,如果有25個以上的key過期了,立刻額外隨機選取下100個key(不計算在10次之內)。可見,如果過期的key不多,它最多每秒回收200條左右,如果有超過25%的key過期了,它就會做得更多,但只要key不被主動get,它占用的內存什么時候最終被清理掉只有天知道。
常用操作
-
Key的長度限制:Key的最大長度不能超過1024字節,在實際開發時不要超過這個長度,但是Key的命名不能太短,要能唯一標識緩存的對,作者建議按照在關系型數據庫中的庫表唯一標識字段的方式來命令Key的值,用分號分割不同數據域,用點號作為單詞連接。
-
Key的查詢:Keys,返回匹配的key,支持通配符如 “keys a*” 、 “keys a?c”,但不建議在生產環境大數據量下使用。
-
對Key對應的Value進行的排序:Sort命令對集合按數字或字母順序排序后返回或另存為list,還可以關聯到外部key等。因為復雜度是最高的O(N+Mlog(M))*(N是集合大小,M 為返回元素的數量),有時會安排到slave上執行。官網鏈接https://redis.io/commands/sort
-
Key的超時操作:Expire(指定失效的秒數)/ExpireAt(指定失效的時間戳)/Persist(持久化)/TTL(返回還可存活的秒數),關於Key超時的操作。默認以秒為單位,也有p字頭的以毫秒為單位的版本
4- String(字符串類型的Value)
可以是String,也可是是任意的byte[]類型的數組,如圖片等。String 在 redis 內部存儲默認就是一個字符串,被 redisObject 所引用,當遇到 incr,decr 等操作時會轉成數值型進行計算,此時 redisObject 的 encoding 字段為int。
https://redis.io/commands#string
-
大小限制:最大為512Mb,基本可以存儲任意圖片啦。
-
常用命令的時間復雜度為O(1),讀寫一樣的快。
-
對String代表的數字進行增減操作(沒有指定的Key則設置為0值,然后在進行操作):Incr/IncrBy/IncrByFloat/Decr/DecrBy(原子性),** 可以用來做計數器,做自增序列,也可以用於限流,令牌桶計數等**。key不存在時會創建並貼心的設原值為0。IncrByFloat專門針對float。。
-
設置Value的安全性:SetNx命令僅當key不存在時才Set(原子性操作)。可以用來選舉Master或做分布式鎖:所有Client不斷嘗試使用SetNx master myName搶注Master,成功的那位不斷使用Expire刷新它的過期時間。如果Master倒掉了key就會失效,剩下的節點又會發生新一輪搶奪。SetEx, Set + Expire 的簡便寫法,p字頭版本以毫秒為單位。
-
獲取:GetSet(原子性), 設置新值,返回舊值。比如一個按小時計算的計數器,可以用GetSet獲取計數並重置為0。這種指令在服務端做起來是舉手之勞,客戶端便方便很多。MGet/MSet/MSetNx, 一次get/set多個key。
-
其他操作:Append/SetRange/GetRange/StrLen,對文本進行擴展、替換、截取和求長度,只對特定數據格式如字段定長的有用,json就沒什么用。
-
BitMap的用法:GetBit/SetBit/BitOp,與或非/BitCount, BitMap的玩法,比如統計今天的獨立訪問用戶數時,每個注冊用戶都有一個offset,他今天進來的話就把他那個位設為1,用BitCount就可以得出今天的總人數。
5- Hash(HashMap,哈希映射表)
Redis 的 Hash 實際是內部存儲的 Value 為一個 HashMap,並提供了直接存取這個 Map 成員的接口。Hash將對象的各個屬性存入Map里,可以只讀取/更新對象的某些屬性。另外不同的模塊可以只更新自己關心的屬性而不會互相並發覆蓋沖突。
![]()
不同程序通過 key(用戶 ID) + field(屬性標簽)就可以並發操作各自關心的屬性數據
https://redis.io/commands#hash
實現原理
Redis Hash 對應 Value 內部實際就是一個 HashMap,實際這里會有2種不同實現,** 這個 Hash 的成員比較少時 Redis 為了節省內存會采用類似一維數組的方式來緊湊存儲,而不會采用真正的 HashMap 結構,對應的 value redisObject 的 encoding 為 zipmap,當成員數量增大時會自動轉成真正的 HashMap,此時 encoding 為 ht**。一般操作復雜度是O(1),要同時操作多個field時就是O(N),N是field的數量。
常用操作
- O(1)操作:hget、hset等等
- O(n)操作:hgetallRedis 可以直接取到全部的屬性數據,但是如果內部 Map 的成員很多,那么涉及到遍歷整個內部 Map 的操作,由於 Redis 單線程模型的緣故,這個遍歷操作可能會比較耗時,而另其它客戶端的請求完全不響應,這點需要格外注意。
6- List(雙向鏈表)
Redis list 的應用場景非常多,也是 Redis 最重要的數據結構之一,比如 twitter 的關注列表,粉絲列表等都可以用 Redis 的 list 結構來實現,還提供了生產者消費者阻塞模式(B開頭的命令),常用於任務隊列,消息隊列等。
實現方式
Redis list 的實現為一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷,Redis 內部的很多實現,包括發送緩沖隊列等也都是用的這個數據結構。
用作消息隊列中防止數據丟失的解決方法
如果消費者把job給Pop走了又沒處理完就死機了怎么辦?
- 消息生產者保證不丟失
加多一個sorted set,分發的時候同時發到list與sorted set,以分發時間為score,用戶把job做完了之后要用ZREM消掉sorted set里的job,並且定時從sorted set中取出超時沒有完成的任務,重新放回list。 如果發生重復可以在sorted set中在查詢確認一遍,或者將消息的消費接口設計成冪等性。
- 消息消費者保證不丟失
為每個worker多加一個的list,彈出任務時改用RPopLPush,將job同時放到worker自己的list中,完成時用LREM消掉。如果集群管理(如zookeeper)發現worker已經掛掉,就將worker的list內容重新放回主list
常用操作
-
復合操作:RPopLPush/ BRPopLPush,彈出來返回給client的同時,把自己又推入另一個list,是原子操作。
-
按值進行的操作:LRem(按值刪除元素)、LInsert(插在某個值的元素的前后),復雜度是O(N),N是List長度,因為List的值不唯一,所以要遍歷全部元素,而Set只要O(log(N))。
-
按下表進行操作(下標從0開始,隊列從左到右算,下標為負數時則從右到左,-1為右端第一個元素)
時間復雜度為O(N)
- LSet :按下標設置元素值。(N為List的長度)
- LIndex:按下標返回元素。(N為index的值)
-
LTrim:限制List的大小,保留指定范圍的元素。(N是移除元素的個數)
-
LRange:返回列表內指定范圍下標的元素,常用於分頁。(N = start+range)
7- set(HashSet)
Set就是HashSet,可以將重復的元素隨便放入而Set會自動去重,底層實現也是HashMap,並且 set 提供了判斷某個成員是否在一個 set 集合內的重要接口,這個也是 list 所不能提供的。
實現原理
set 的內部實現是一個 value 永遠為 null 的 HashMap,實際就是通過計算 hash 的方式來快速排重的,這也是 set 能提供判斷一個成員是否在集合內的原因。
常用操作
-
增刪改查:SAdd/SRem/SIsMember/SCard/SMove/SMembers等等。除了SMembers都是O(1)。
-
集合操作:SInter/SInterStore/SUnion/SUnionStore/SDiff/SDiffStore,各種集合操作。交集運算可以用來顯示在線好友(在線用戶 交集 好友列表),共同關注(兩個用戶的關注列表的交集)。O(N),並集和差集的N是集合大小之和,交集的N是小的那個集合的大小的2倍。
8- Sorted Set(插入有序Set集合)
set 不是自動有序的,而** sorted set 可以通過用戶額外提供一個優先級(score)的參數來為成員排序,並且是插入有序的,即自動排序**。當你需要一個有序的並且不重復的集合列表,那么可以選擇 sorted set 數據結構,比如 twitter 的 public timeline 可以以發表時間作為 score 來存儲,這樣獲取時就是自動按時間排好序的。
實現方式
內部使用 HashMap 和跳躍表(SkipList)來保證數據的存儲和有序
Sorted Set的實現是HashMap(element->score, 用於實現ZScore及判斷element是否在集合內),和SkipList(score->element,按score排序)的混合體。SkipList有點像平衡二叉樹那樣,不同范圍的score被分成一層一層,每層是一個按score排序的鏈表。
常用操作
ZAdd/ZRem是O(log(N));ZRangeByScore/ZRemRangeByScore是O(log(N)+M),N是Set大小,M是結果/操作元素的個數。復雜度的log取對數很關鍵,可以使,1000萬大小的Set,復雜度也只是幾十不到。但是,如果一次命中很多元素M很大則復雜度很高。
-
ZRange/ZRevRange,按排序結果的范圍返回元素列表,可以為正數與倒數。
-
ZRangeByScore/ZRevRangeByScore,按score的范圍返回元素,可以為正數與倒數。
-
ZRemRangeByRank/ZRemRangeByScore,按排序/按score的范圍限刪除元素。
-
ZCount,統計按score的范圍的元素個數。
-
ZRank/ZRevRank ,顯示某個元素的正/倒序的排名。
-
ZScore/ZIncrby,顯示元素的Score值/增加元素的Score。
-
ZAdd(Add)/ZRem(Remove)/ZCard(Count),ZInsertStore(交集)/ZUnionStore(並集),與Set相比,少了IsMember和差集運算。
8- Redis使用與內存優化
上面的一些實現上的分析可以看出 redis 實際上的內存管理成本非常高,即占用了過多的內存,屬於用空間換時間。作者對這點也非常清楚,所以提供了一系列的參數和手段來控制和節省內存
建議不要開啟VM(虛擬內存)選項
VM 選項是作為 Redis 存儲超出物理內存數據的一種數據在內存與磁盤換入換出的一個持久化策略,將嚴重地拖垮系統的運行速度,所以要關閉 VM 功能,請檢查你的 redis.conf 文件中 vm-enabled 為 no。
設置最大內存選項
最好設置下 redis.conf 中的 maxmemory 選項,該選項是告訴 Redis 當使用了多少物理內存后就開始拒絕后續的寫入請求,該參數能很好的保護好你的 Redis 不會因為使用了過多的物理內存而導致 swap,最終嚴重影響性能甚至崩潰。
一般還需要設置內存飽和回收策略
- volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
- volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
- volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
- allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰
- allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
- no-enviction(驅逐):禁止驅逐數據
控制內存使用的參數
Redis 為不同數據類型分別提供了一組參數來控制內存使用
- Hash
redis.conf 配置文件中下面2項
- **hash-max-zipmap-entries 64 **
含義是當 value 這個 Map 內部不超過多少個成員時會采用線性緊湊格式存儲,默認是64,即 value 內部有64個以下的成員就是使用線性緊湊存儲zipmap,超過該值自動轉成真正的 HashMap(ht)。
- hash-max-zipmap-value 512
hash-max-zipmap-value 含義是當 value 這個 Map 內部的每個成員值長度不超過
多少字節就會采用線性緊湊存儲zipmap來節省空間。
以上2個條件任意一個條件超過設置值都會轉換成真正的 HashMap,也就不會再節省內存了,但是也不是越大越好(空間和查改效率需要根據實際情況來權衡)
- List
- list-max-ziplist-entries 512
list 數據類型多少節點以下會采用去指針的緊湊存儲格式ziplist - list-max-ziplist-value 64
list 數據類型節點值大小小於多少字節會采用緊湊存儲格式ziplist。
- Set
- set-max-intset-entries 512
set 數據類型內部數據如果全部是數值型,且包含多少節點以下會采用緊湊格式存儲
Redis內部的優化
-
Redis 內部實現沒有對內存分配方面做過多的優化,在一定程度上會存在內存碎片,不過大多數情況下這個不會成為 Redis 的性能瓶頸。
-
Redis 緩存了一定范圍的常量數字作為資源共享,在很多數據類型是數值型則能極大減少內存開銷,默認為1-10000,可以重新編譯配置修改源代碼中的一行宏定義 REDIS_SHARED_INTEGERS。
9- 總結
-
根據業務需要選擇合適的數據類型,並為不同的應用場景設置相應的緊湊存儲參數。
-
當業務場景不需要數據持久化時,關閉所有的持久化方式可以獲得最佳的性能以及最大的內存使用量。
-
如果需要使用持久化,根據是否可以容忍重啟丟失部分數據在快照方式與語句追加方式之間選擇其一,不要使用虛擬內存以及 diskstore 方式。
-
不要讓你的 Redis 所在機器物理內存使用超過實際內存總量的3/5。
Redis 的持久化使用了 Buffer IO ,所謂 Buffer IO 是指 Redis 對持久化文件的寫入和讀取操作都會使用物理內存的 Page Cache,而當 Redis 的持久化文件過大操作系統會進行Swap,這時你的系統就會有內存還有余量但是系統不穩定或者崩潰的風險。
作者:zhanglbjames
鏈接:https://www.jianshu.com/p/f09480c05e42
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
