本文目錄一覽:
4、Redis高性能的根本原理
內存的讀寫速度很快
Epoll 模型
常用的五大Redis的數據結構,及他們各自的底層實現結構
string hash list set sortset(zset)
string 的底層實現是 簡單動態字符串(SDS -simple dynamic string)
hash 的底層實現是 hash表 或則 壓縮列表(ziplist)
list 的底層實現是 雙向列表(quicklist) 或者 壓縮列表
set 的底層實現是 hash表(hashtable) 或者 整數數組
sortset(zset) 的底層實現是 壓縮列表 或者 跳錶
各個數據結構的底層實現概覽
value是 string 類型的時候分為三種情況
(1)、當設置的值是整數類型的時候,redis底層會將 string 類型轉化為 int 來存儲
(2)、設置的值小於等於44個字節的時候,使用的編碼是 embstr
(3)、設置的值大於44個字節的時候,使用的編碼是 raw
redis是用C語言編寫的,在C語言中 string 類型是用字符數組 char[] 來實現的。redis實現字符串的底層並沒有直接使用C語言中的字符數組的形式,而是進行了改造,構造出了一種SDS的數據結構
list的底層使用 快速雙向鏈表quicklist 或者 壓縮鏈表ziplist 來實現的。
list的底層並沒有使用傳統的雙向鏈表的結構是因為
(1)、雙向鏈表需要有一個 前指針 和 後指針 ,每個指針佔用的空間分別都是8byte, 佔用內存 比較多
(2)、雙向鏈表所通用的一個問題是會形成很多的 內存碎片
壓縮鏈表 ziplist 結構是
快速雙向鏈表 quicklist 結構
hash的底層實現為 hashtable 或者 ziplist 。
hashtable的底層實現
當數據量比較小或者單個元素的時候,底層使用的是ziplist存儲,具體可以通過配置來制定
1、 hashtable 是無序的 ziplist 是有序的
2、在能使用 hash 的情況下優先使用 hash ,不要使用 String ,因為使用太多的 String ,則會創建出過多的 key ,當 key 大量的時候,就會容易發生 hash碰撞 ,所以就需要頻繁的 rehash ,每次 rehash 就會創建2倍的內存,造成內存浪費
hash的底層實現為 整數數組intset 或者 hashtable 。
當set都為整數的時候,set的底層實現都是使用 intset 結構實現
如果set中存在字符串的值,則使用 hashtable 來實現
intset 是有序的, hashtable 是無序的
sortset 底層使用 壓縮列表ziplist 或 跳錶skiplist 的結構實現
當數據量小的情況下,使用 ziplist 實現,當數據量大的情況下使用 ziplist 實現,具體可以通過配置設置
默認設置下的底層結構
skiplist 的底層實現
查找對應元素的時候,先從最高的索引層找,例如找c 150,則先從L1找,L1的指針指向b,查看b120小於150,則繼續往後找,b的指針指向null,則向下一層找,向下一層b的指針指向c,查看c的score為150,所以找到對應的元素c
1、
Redis的內存優化
一. redisObject對象
二. 縮減鍵值對象
三. 共享對象池
四. 字符串優化
五. 編碼優化
六. 控制key的數量
Redis存儲的所有值對象在內部定義為redisObject結構體,內部結構如下圖所示。
表示當前對象使用的數據類型,Redis主要支持5種數據類型:string,hash,list,set,zset。可以使用type {key}命令查看對象所屬類型,type命令返回的是值對象類型,鍵都是string類型。
表示Redis內部編碼類型,encoding在Redis內部使用,代表當前對象內部採用哪種數據結構實現。理解Redis內部編碼方式對於優化內存非常重要 ,同一個對象採用不同的編碼實現內存佔用存在明顯差異,具體細節見之後編碼優化部分。
記錄對象最後一次被訪問的時間,當配置了 maxmemory和maxmemory-policy=volatile-lru | allkeys-lru 時, 用於輔助LRU算法刪除鍵數據。可以使用object idletime {key}命令在不更新lru字段情況下查看當前鍵的空閑時間。
記錄當前對象被引用的次數,用於通過引用次數回收內存,當refcount=0時,可以安全回收當前對象空間。使用object refcount {key}獲取當前對象引用。當對象為整數且範圍在[0-9999]時,Redis可以使用共享對象的方式來節省內存。具體細節見之後共享對象池部分。
與對象的數據內容相關,如果是整數直接存儲數據,否則表示指向數據的指針。Redis在3.0之後對值對象是字符串且長度=39字節的數據,內部編碼為embstr類型,字符串sds和redisObject一起分配,從而只要一次內存操作。
降低Redis內存使用最直接的方式就是縮減鍵(key)和值(value)的長度。
其中java-built-in-serializer表示JAVA內置序列化方式,更多數據見jvm-serializers項目: ,其它語言也有各自對應的高效序列化工具。
值對象除了存儲二進制數據之外,通常還會使用通用格式存儲數據比如:json,xml等作為字符串存儲在Redis中。這種方式優點是方便調試和跨語言,但是同樣的數據相比字節數組所需的空間更大,在內存緊張的情況下,可以使用通用壓縮算法壓縮json,xml後再存入Redis,從而降低內存佔用,例如使用GZIP壓縮後的json可降低約60%的空間。
對象共享池指Redis內部維護[0-9999]的整數對象池。創建大量的整數類型redisObject存在內存開銷,每個redisObject內部結構至少佔16字節,甚至超過了整數自身空間消耗。所以Redis內存維護一個[0-9999]的整數對象池,用於節約內存。 除了整數值對象,其他類型如list,hash,set,zset內部元素也可以使用整數對象池。因此開發中在滿足需求的前提下,盡量使用整數對象以節省內存。
整數對象池在Redis中通過變量REDIS_SHARED_INTEGERS定義,不能通過配置修改。可以通過object refcount 命令查看對象引用數驗證是否啟用整數對象池技術,如下:
設置鍵foo等於100時,直接使用共享池內整數對象,因此引用數是2,再設置鍵bar等於100時,引用數又變為3,如下圖所示。
使用整數對象池究竟能降低多少內存?讓我們通過測試來對比對象池的內存優化效果,如下表所示。
使用共享對象池後,相同的數據內存使用降低30%以上。可見當數據大量使用[0-9999]的整數時,共享對象池可以節約大量內存。需要注意的是對象池並不是只要存儲[0-9999]的整數就可以工作。當設置maxmemory並啟用LRU相關淘汰策略如:volatile-lru,allkeys-lru時,Redis禁止使用共享對象池,測試命令如下:
LRU算法需要獲取對象最後被訪問時間,以便淘汰最長未訪問數據,每個對象最後訪問時間存儲在redisObject對象的lru字段。對象共享意味着多個引用共享同一個redisObject,這時lru字段也會被共享,導致無法獲取每個對象的最後訪問時間。如果沒有設置maxmemory,直到內存被用盡Redis也不會觸發內存回收,所以共享對象池可以正常工作。
綜上所述,共享對象池與maxmemory+LRU策略衝突,使用時需要注意。 對於ziplist編碼的值對象,即使內部數據為整數也無法使用共享對象池,因為ziplist使用壓縮且內存連續的結構,對象共享判斷成本過高,ziplist編碼細節後面內容詳細說明。
首先整數對象池復用的幾率最大,其次對象共享的一個關鍵操作就是判斷相等性,Redis之所以只有整數對象池,是因為整數比較算法時間複雜度為O(1),只保留一萬個整數為了防止對象池浪費。如果是字符串判斷相等性,時間複雜度變為O(n),特別是長字符串更消耗性能(浮點數在Redis內部使用字符串存儲)。對於更複雜的數據結構如hash,list等,相等性判斷需要O(n2)。對於單線程的Redis來說,這樣的開銷顯然不合理,因此Redis只保留整數共享對象池。
字符串對象是Redis內部最常用的數據類型。所有的鍵都是字符串類型, 值對象數據除了整數之外都使用字符串存儲。比如執行命令:lpush cache:type “redis” “memcache” “tair” “levelDB” ,Redis首先創建”cache:type”鍵字符串,然後創建鏈表對象,鏈表對象內再包含四個字符串對象,排除Redis內部用到的字符串對象之外至少創建5個字符串對象。可見字符串對象在Redis內部使用非常廣泛,因此深刻理解Redis字符串對於內存優化非常有幫助:
Redis沒有採用原生C語言的字符串類型而是自己實現了字符串結構,內部簡單動態字符串(simple dynamic string),簡稱SDS。結構下圖所示。
Redis自身實現的字符串結構有如下特點:
因為字符串(SDS)存在預分配機制,日常開發中要小心預分配帶來的內存浪費,例如下表的測試用例。
從測試數據可以看出,同樣的數據追加後內存消耗非常嚴重,下面我們結合圖來分析這一現象。階段1每個字符串對象空間佔用如下圖所示。
階段1插入新的字符串後,free字段保留空間為0,總佔用空間=實際佔用空間+1字節,最後1字節保存‘\0’標示結尾,這裡忽略int類型len和free字段消耗的8字節。在階段1原有字符串上追加60字節數據空間佔用如下圖所示。
追加操作後字符串對象預分配了一倍容量作為預留空間,而且大量追加操作需要內存重新分配,造成內存碎片率(mem_fragmentation_ratio)上升。直接插入與階段2相同數據的空間佔用,如下圖所示。
階段3直接插入同等數據後,相比階段2節省了每個字符串對象預分配的空間,同時降低了碎片率。
字符串之所以採用預分配的方式是防止修改操作需要不斷重分配內存和字節數據拷貝。但同樣也會造成內存的浪費。字符串預分配每次並不都是翻倍擴容,空間預分配規則如下:
字符串重構:指不一定把每份數據作為字符串整體存儲,像json這樣的數據可以使用hash結構,使用二級結構存儲也能幫我們節省內存。同時可以使用hmget,hmset命令支持字段的部分讀取修改,而不用每次整體存取。例如下面的json數據:
分別使用字符串和hash結構測試內存表現,如下表所示。
根據測試結構,第一次默認配置下使用hash類型,內存消耗不但沒有降低反而比字符串存儲多出2倍,而調整hash-max-ziplist-value=66之後內存降低為535.60M。因為json的videoAlbumPic屬性長度是65,而hash-max-ziplist-value默認值是64,Redis採用hashtable編碼方式,反而消耗了大量內存。調整配置後hash類型內部編碼方式變為ziplist,相比字符串更省內存且支持屬性的部分操作。下一節將具體介紹ziplist編碼優化細節。
Redis對外提供了string,list,hash,set,zet等類型,但是Redis內部針對不同類型存在編碼的概念,所謂編碼就是具體使用哪種底層數據結構來實現。編碼不同將直接影響數據的內存佔用和讀寫效率。使用object encoding {key}命令獲取編碼類型。如下:
Redis針對每種數據類型(type)可以採用至少兩種編碼方式來實現,下表表示type和encoding的對應關係。
了解編碼和類型對應關係之後,我們不禁疑惑Redis為什麼需要對一種數據結構實現多種編碼方式?
主要原因是Redis作者想通過不同編碼實現效率和空間的平衡。比如當我們的存儲只有10個元素的列表,當使用雙向鏈表數據結構時,必然需要維護大量的內部字段如每個元素需要:前置指針,後置指針,數據指針等,造成空間浪費,如果採用連續內存結構的壓縮列表(ziplist),將會節省大量內存,而由於數據長度較小,存取操作時間複雜度即使為O(n2)性能也可滿足需求。
Redis內存優化
編碼類型轉換在Redis寫入數據時自動完成,這個轉換過程是不可逆的,轉換規則只能從小內存編碼向大內存編碼轉換。例如:
以上命令體現了list類型編碼的轉換過程,其中Redis之所以不支持編碼回退,主要是數據增刪頻繁時,數據向壓縮編碼轉換非常消耗CPU,得不償失。以上示例用到了list-max-ziplist-entries參數,這個參數用來決定列表長度在多少範圍內使用ziplist編碼。當然還有其它參數控制各種數據類型的編碼,如下表所示:
掌握編碼轉換機制,對我們通過編碼來優化內存使用非常有幫助。下面以hash類型為例,介紹編碼轉換的運行流程,如下圖所示。
理解編碼轉換流程和相關配置之後,可以使用config set命令設置編碼相關參數來滿足使用壓縮編碼的條件。對於已經採用非壓縮編碼類型的數據如hashtable,linkedlist等,設置參數後即使數據滿足壓縮編碼條件,Redis也不會做轉換,需要重啟Redis重新加載數據才能完成轉換。
ziplist編碼主要目的是為了節約內存,因此所有數據都是採用線性連續的內存結構。ziplist編碼是應用範圍最廣的一種,可以分別作為hash、list、zset類型的底層數據結構實現。首先從ziplist編碼結構開始分析,它的內部結構類似這樣:….。一個ziplist可以包含多個entry(元素),每個entry保存具體的數據(整數或者字節數組),內部結構如下圖所示。
ziplist結構字段含義:
根據以上對ziplist字段說明,可以分析出該數據結構特點如下:
下面通過測試展示ziplist編碼在不同類型中內存和速度的表現,如下表所示。
測試數據採用100W個36字節數據,劃分為1000個鍵,每個類型長度統一為1000。從測試結果可以看出:
intset編碼是集合(set)類型編碼的一種,內部表現為存儲有序,不重複的整數集。當集合只包含整數且長度不超過set-max-intset-entries配置時被啟用。執行以下命令查看intset表現:
以上命令可以看出intset對寫入整數進行排序,通過O(log(n))時間複雜度實現查找和去重操作,intset編碼結構如下圖所示。
intset的字段結構含義:
根據以上測試結果發現intset表現非常好,同樣的數據內存佔用只有不到hashtable編碼的十分之一。intset數據結構插入命令複雜度為O(n),查詢命令為O(log(n)),由於整數佔用空間非常小,所以在集合長度可控的基礎上,寫入命令執行速度也會非常快,因此當使用整數集合時盡量使用intset編碼。上表測試第三行把ziplist-hash類型也放入其中,主要因為intset編碼必須存儲整數,當集合內保存非整數數據時,無法使用intset實現內存優化。這時可以使用ziplist-hash類型對象模擬集合類型,hash的field當作集合中的元素,value設置為1字節佔位符即可。使用ziplist編碼的hash類型依然比使用hashtable編碼的集合節省大量內存。
當使用Redis存儲大量數據時,通常會存在大量鍵,過多的鍵同樣會消耗大量內存。Redis本質是一個數據結構服務器,它為我們提供多種數據結構,如hash,list,set,zset 等結構。使用Redis時不要進入一個誤區,大量使用get/set這樣的API,把Redis當成Memcached使用。對於存儲相同的數據內容利用Redis的數據結構降低外層鍵的數量,也可以節省大量內存。如下圖所示,通過在客戶端預估鍵規模,把大量鍵分組映射到多個hash結構中降低鍵的數量。
hash結構降低鍵數量分析:
通過這個測試數據,可以說明:
關於hash鍵和field鍵的設計:
使用hash結構控制鍵的規模雖然可以大幅降低內存,但同樣會帶來問題,需要提前做好規避處理。如下:
本文主要講解Redis內存優化技巧,Redis的數據特性是”ALL IN MEMORY”,優化內存將變得非常重要。對於內存優化建議讀者先要掌握Redis內存存儲的特性比如字符串,壓縮編碼,整數集合等,再根據數據規模和所用命令需求去調整,從而達到空間和效率的最佳平衡。建議使用Redis存儲大量數據時,把內存優化環節加入到前期設計階段,否則數據大幅增長後,開發人員需要面對重新優化內存所帶來開發和數據遷移的雙重成本。當Redis內存不足時,首先考慮的問題不是加機器做水平擴展,應該先嘗試做內存優化。當遇到瓶頸時,再去考慮水平擴展。即使對於集群化方案,垂直層面優化也同樣重要,避免不必要的資源浪費和集群化後的管理成本。
redisjson與es的優缺點
1、redisjson優點:速度快,完全基於內存,使用C語言實現,網絡層使用epoll解決高並發問題。缺點:短時間內大量增加數據,可能導致內存不夠用。
2、ES優點:會建立一個覆蓋表中所有文檔、所有字段的龐大的倒排索引,以實現對存入ES中的所有數據進行快速檢索。缺點:字段類型無法修改、寫入性能較低和高硬件資源消耗。
什麼是Redis?
REmote DIctionary Server(Redis) 是一個由Salvatore Sanfilippo寫的key-value存儲系統
Redis是一個開源的使用ANSIC語言編寫、遵守BSD協議、支持網絡、可基於內存亦可持久化的日誌型、Key-Value數據庫,並提供多種語言的API
它通常被稱為數據結構服務器,因為值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets)和有序集合(sorted sets)等類型
Redis 簡介
Redis是完全開源免費的,遵守BSD協議,是一個高性能的key-value數據庫
Redis與其他key – value緩存產品有以下三個特點:
①Redis支持數據的持久化,可以將內存中的數據保存在磁盤中,重啟的時候可以再次加載進行使用。
②Redis不僅僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
③Redis支持數據的備份,即master-slave模式的數據備份。
Redis 的特點
高性能:Redis 將所有數據集存儲在內存中,可以在入門級 Linux 機器中每秒寫(SET)11 萬次,讀(GET)8.1 萬次
持久化:當所有數據都存在於內存中時,可以根據自上次保存以來經過的時間和/或更新次數,使用靈活的策略將更改異步保存在磁盤上
數據結構:Redis 支持各種類型的數據結構,例如字符串、散列、集合、列表、帶有範圍查詢的有序集、位圖、超級日誌和帶有半徑查詢的地理空間索引
原子操作:處理不同數據類型的 Redis 操作是原子操作,因此可以安全地 SET 或 INCR 鍵,添加和刪除集合中的元素等
支持的語言:Redis 支持許多語言,如C、C++、Erlang、Go、Haskell、Java、JavaScript(Node.js)、Lua、Objective-C、Perl、PHP、Python、R、Ruby、Rust、Scala、Smalltalk等
主/從複製:Redis 遵循非常簡單快速的主/從複製。配置文件中只需要一行來設置它,而 Slave 在 Amazon EC2 實例上完成 10 MM
key 集的初始同步只需要 21 秒
分片:Redis 支持分片。與其他鍵值存儲一樣,跨多個 Redis 實例分發數據集非常容易
可移植:Redis 是用 C 編寫的,適用於大多數 POSIX 系統,如 Linux、BSD、Mac OS X、Solaris 等
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/249391.html