1. 程式人生 > >降低Redis內存占用

降低Redis內存占用

服務器 硬件

1、降低redis內存占用的優點

  1、有助於減少創建快照和加載快照所用的時間

  2、提升載入AOF文件和重寫AOF文件時的效率

  3、縮短從服務器進行同步所需的時間

  4、無需添加額外的硬件就可以讓redis存貯更多的數據

回到頂部

2、短結構

  Redis為列表、集合、散列、有序集合提供了一組配置選項,這些選項可以讓redis以更節約的方式存儲較短的結構。

回到頂部

  2.1、ziplist壓縮列表(列表、散列、有續集和)

  通常情況下使用的存儲方式

技術分享

  當列表、散列、有序集合的長度較短或者體積較小的時候,redis將會采用一種名為ziplist的緊湊存儲方式來存儲這些結構。

  ziplist是列表、散列、有序集合這三種不同類型的對象的一種非結構化表示,它會以序列化的方式存儲數據,這些序列化的數據每次被讀取的時候都需要進行解碼,每次寫入的時候也要進行編碼。

  雙向列表與壓縮列表的區別:

  為了了解壓縮列表比其他數據結構更加節約內存,我們以列表結構為例進行深入研究。

  典型的雙向列表

    在典型雙向列表裏面,每個值都都會有一個節點表示。每個節點都會帶有指向鏈表前一個節點和後一個節點的指針,以及一個指向節點包含的字符串值的指針。

    每個節點包含的字符串值都會分為三部分進行存儲。包括字符串長度、字符串值中剩余可用字節數量、以空字符結尾的字符串本身。

  例子:

  假若一個某個節點存儲了’abc’字符串,在32位的平臺下保守估計需要21個字節的額外開銷(三個指針+兩個int+空字符即:3*4+2*4+1=21)

  由例子可知存儲一個3字節字符串就需要付出至少21個字節的額外開銷。

  ziplist

    壓縮列表是由節點組成的序列,每個節點包含兩個長度和一個字符串。第一個長度記錄前一個節點的長度(用於對壓縮列表從後向前遍歷);第二個長度是記錄本當前點的長度;被存儲的字符串。

  例子:

  存儲字符串’abc’,兩個長度都可以用1字節來存儲,因此所帶來的額外開銷為2字節(兩個長度即1+1=2)

  結論:

  壓縮列表是通過避免存儲額外的指針和元數據,從而達到降低額外的開銷。

  配置:

技術分享

1 #list2 list-max-ziplist-entries 512  #表示允許包含的最大元素數量3 list-max-ziplist-value 64    #表示壓縮節點允許存儲的最大體積4 #hash                  #當超過任一限制後,將不會使用ziplist方式進行存儲5 hash-max-ziplist-entries 5126 hash-max-ziplist-value 647 #zset8 zset-max-ziplist-entries 1289 zset-max-ziplist-value 64

技術分享

 測試list:

1、建立test.php文件

技術分享

1 #test.php2 <?php3 $redis=new Redis();4 $redis->connect(‘192.168.95.11‘,‘6379‘);5 for ($i=0; $i<512  ; $i++) 
6 { 
7     $redis->lpush(‘test-list‘,$i.‘-test-list‘);  #往test-list推入512條數據8 }9 ?>

技術分享

技術分享

  此時的test-list中含有512條數據,沒有超除配置文件中的限制

2、往test-list中再推入一條數據

技術分享

  此時test-list含有513條數據,大於配置文件中限制的512條,索引將放棄ziplist存儲方式,采用其原來的linkedlist存儲方式

  散列與有序集合同理。

回到頂部

  2.2、intset整數集合(集合)

  前提條件,集合中包含的所有member都可以被解析為十進制整數。

  以有序數組的方式存儲集合不僅可以降低內存消耗,還可以提升集合操作的執行速度。

  配置:

1 set-max-intset-entries  512   #限制集合中member個數,超出則不采取intset存儲

  測試:

  建立test.php文件

技術分享

1 #test.php2 <?php3 $redis=new Redis();4 $redis->connect(‘192.168.95.11‘,‘6379‘);5 for ($i=0; $i<512  ; $i++) 
6 { 
7     $redis->sadd(‘test-set‘,$i);   #給集合test-set插入512個member8 }9 ?>

技術分享

技術分享

回到頂部

  2.3、性能問題

  不管列表、散列、有序集合、集合,當超出限制的條件後,就會轉換為更為典型的底層結構類型。因為隨著緊湊結構的體積不斷變大,操作這些結構的速度將會變得越來越慢。

  測試:

#將采用list進行代表性測試

測試思路:

1、在默認配置下往test-list推入50000條數據,查看所需時間;接著在使用rpoplpush將test-list數據全部推入到新列表list-new中,查看所需時間

2、修改配置,list-max-ziplist-entries 100000,再執行上面的同樣操作

3、對比時間,得出結論

  默認配置下測試:

  1、插入數據,查看時間

技術分享

 1 #test1.php 2 <?php 3 header("content-type: text/html;charset=utf8;"); 4 $redis=new Redis(); 5 $redis->connect(‘192.168.95.11‘,‘6379‘); 6 $start=time(); 7 for ($i=0; $i<50000  ; $i++) 
 8 { 
 9     $redis->lpush(‘test-list‘,$i.‘-aaaassssssddddddkkk‘);10 }11 $end=time();12 echo "插入耗時為:".($end-$start).‘s‘;13 ?>

技術分享

技術分享技術分享

結果耗時4秒

  2、執行相應命令,查看耗時

技術分享

 1 #test2.php 2 <?php 3 header("content-type: text/html;charset=utf8;"); 4 $redis=new Redis(); 5 $redis->connect(‘192.168.95.11‘,‘6379‘); 6 $start=time(); 7 $num=0; 8 while($redis->rpoplpush(‘test-list‘,‘test-new‘)) 9 {10     $num+=1;11 }12 echo ‘執行次數為:‘.$num."<br/>";13 $end=time();14 echo "耗時為:".($end-$start).‘s‘;15 ?>

技術分享

技術分享

  更改配置文件下測試  

  1、先修改配置文件

  list-max-ziplist-entries 100000 #將這個值修改大一點,可以更好的凸顯對性能的影響

  list-max-ziplist-value 64 #此值可不做修改

  2、插入數據

  執行test1.php

  結果為:耗時12s

  技術分享

  3、執行相應命令,查看耗時

  執行test2.php

  結果為:執行次數:50000,耗時12s

結論:

在本機中執行測試50000條數據就相差8s,若在高並發下,長壓縮列表和大整數集合將起不到任何的優化,反而使得性能降低。

回到頂部

3、片結構

  分片的本質就是基於簡單的規則將數據劃分為更小的部分,然後根據數據所屬的部分來決定將數據發送到哪個位置上。很多數據庫使用這種技術來擴展存儲空間,並提高自己所能處理的負載量。

  結合前面講到的,我們不難發現分片結構對於redis的重要意義。因此我們需要在配置文件中關於ziplist以及intset的相關配置做出適當的調整。

回到頂部

  3.1、分片式散列

  #ShardHash.class.php

技術分享 View Code

  散列分片主要是根據基礎鍵以及散列包含的鍵計算出分片鍵ID,然後再與基礎鍵拼接成一個完整的分片鍵。在執行hset與hget以及大部分hash命令時,都需要先將key(field)通過shardKey方法處理,得到分片鍵才能夠進行下一步操作。

回到頂部

  3.2、分片式集合

  如何構造分片式集合才能夠讓它更節省內存,性能更加強大呢?主要的思路就是,將集合裏面的存儲的數據盡量在不改變其原有功能的情況下轉換成可以被解析為十進制的數據。根據前面所講到的,當集合中的所有成員都能夠被解析為十進制數據時,將會采用intset存儲方式,這不僅能夠節省內存,而且還可以提高響應的性能。

例子:

假若要某個大型網站需要存儲每一天的唯一用戶訪問量。那麽就可以使用將用戶的唯一標識符轉化成十進制數字,再存入分片式set中。

#ShardSet.class.php

技術分享 View Code

回到頂部

4、將信息打包轉換成存儲字節

  結合前面所講的分片技術,采用string分片結構為大量連續的ID用戶存儲信息。

  使用定長字符串,為每一個ID分配n個字節進行存儲相應的信息。

  接下來我們將采用存儲用戶國家、省份的例子進行講解:

  假若某個用戶需要存儲中國、廣東省這兩個信息,采用utf8字符集,那麽至少需要消耗5*3=15個字節。如果網站的用戶量大的話,這樣的做法將會占用很多資源。接下來我們采用的方法每個用戶僅僅只需要占用兩個字節就可以完成存儲信息。

  具體思路步驟:

  1、首先我們為國家、以及各國家的省份信息建立相應的’信息表格’

  2、將’信息表格’建好後,也意味著每個國家,省份都有相應的索引號

  3、看到這裏大家應該都想到了吧,對就是使用兩個索引作為用戶存儲的信息,不過需要註意的是我們還需要對這兩個索引進行相應的處理

  4、將索引當做ASCII碼,將其轉換為對應ASCII(0~255)所指定的字符

  5、使用前面所講的分片技術,定長分片string結構,將用戶的存儲位置找出來(redis中一個string不能超過512M)

  6、實現信息的寫入以及取出(getrange、setrange)

實現代碼:

#PackBytes.class.php

技術分享

  1 <?php  2 #打包存儲字節  3 #存儲用戶國家、省份信息  4 class PackBytes  5 {  6     private $redis=‘‘;  #存儲redis對象  7     /**  8     * @desc 構造函數  9     * 
 10     * @param $host string | redis主機 11     * @param $port int    | 端口 12     */ 13     public function __construct($host,$port=6379) 14     { 15         $this->redis=new Redis(); 16         $this->redis->connect($host,$port); 17     } 
 18  19     /** 20     * @desc  處理並緩存國家省份數據 21     * @param $countries string | 第一類數據,國家字符串 22     * @param $provinces 二維array  | 第二類數據,各國省份數組 23     * @param $cache 1/0    | 是否使用緩存,默認0不使用 24     * 25     * @return array | 返回總數據 26     */ 27     public function dealData($countries,$provinces,$cache=0) 28     { 29         if($cache) 30         { 31             $result=$this->redis->get(‘cache_data‘); 32             if($result) 33                 return unserialize($result); 34         } 35         $arr=explode(‘ ‘,$countries); 36         $areaArr[]=$arr; 37         $areaArr[]=$provinces; 38         $cache_data=serialize($areaArr); 39         $this->redis->set(‘cache_data‘,$cache_data); 40         return $areaArr; 41     } 42  43     /** 44     * @desc 將具體信息按表索引轉換成編碼信息 45     * 
 46     * @param $countries,$provinces,$cache| 參考dealData方法 47     * @param $country  string             | 具體信息--國家 48     * @param $province   string           | 具體信息--省份 49     * 50     * @return string | 返回轉換的編碼信息 51     */ 52     public function getCode($countries,$provinces,$country,$province,$cache=0) 53     { 54         $dataArr=$this->dealData($countries,$provinces,$cache=0); 55  56         $result=array_search($country, $dataArr[0]);  #查找數組中是否含有data1 57         if($result===false)         #判斷是否存在 58             return chr(0).chr(0);   #不存在則返回初始值 59         $code=chr($result); 60         $result=array_search($province, $dataArr[1][$country]);  #查找數組中是否含有data2 61         if($result===false) 62             return $code.chr(0); 63         return $code.chr($result);      #返回對應ASCII(0~255)所指定的字符  64     } 65  66     /** 67     * @desc 計算用戶存儲編碼數據的相關位置信息 68     * 
 69     * @param $userID int | 用戶的ID 70     * 71     * @return array | 返回一個數組 包含數據存儲時的分片ID、以及屬於用戶的存儲位置(偏移量) 72     */ 73     public function savePosition($userID) 74     { 75         $shardSize=pow(2, 3);      #每個分片的大小 76         $position=$userID*2;        #user的排位 77         $arr[‘shardID‘]=floor($position/$shardSize);   #分片ID 78         $arr[‘offset‘]=$position%$shardSize;      #偏移量 79         return $arr; 80     } 81  82     /** 83     * @desc | 整合方法,將編碼信息存入redis中string相應的位置 84     * 85     * @param $userID int           | 用戶ID 86     * @param $countries string     | 第一類數據,國家字符串 87     * @param $provinces 二維array  | 第二類數據,各國省份數組 88     * @param $country  string             | 具體信息--國家 89     * @param $province   string           | 具體信息--省份 90     * @param $cache 1/0            | 是否使用緩存,默認0不使用 91     * 92     * @return 成功返回寫入位置/失敗false 93     */ 94     public function saveCode($userID,$countries,$provinces,$country,$province,$cache=0) 95     { 96         $code=$this->getCode($countries,$provinces,$country,$province,$cache=0); 97         $arr=$this->savePosition($userID);  #存儲相關位置信息 98         return $this->redis->setrange(‘save_code_‘.$arr[‘shardID‘],$arr[‘offset‘],$code); 99     }100 101     /**102     * @desc 獲取用戶的具體國家與省份信息103     *104     * @param $userID int | 用戶ID105     *106     * @return array | 返回包含國家和省份信息的數組107     */108     public function getMessage($userID)109     {110         $position=$this->savePosition($userID);111         $code=$this->redis->getrange(‘save_code_‘.$position[‘shardID‘],$position[‘offset‘],$position[‘offset‘]+1);112         $arr=str_split($code);113         $areaArr=$this->dealData(‘‘, ‘‘,$cache=1);  #使用緩存數據114         $message[‘country‘]=$areaArr[0][ord($arr[0])];115         $message[‘province‘]=$areaArr[1][$message[‘country‘]][ord($arr[1])];116         return $message;117     }118 119 }120 121 header("content-type: text/html;charset=utf8;");122 $countries="無 中國 日本 越南 朝鮮 俄羅斯 巴基斯坦 美國";123 $provinces=array(124         ‘無‘=>array(‘無‘),125         ‘中國‘=>array(‘無‘,‘廣東‘,‘湖南‘,‘湖北‘,‘廣西‘,‘雲南‘,‘湖南‘,‘河北‘),126         ‘日本‘=>array(‘無‘,‘龜孫子區‘,‘王八區‘,‘倭國鬼區‘,‘鬼子區‘,‘蘿蔔頭區‘),127     );128 $obj=new PackBytes(‘192.168.95.11‘);129 /*130 #數據處理,並將其緩存到redis中131 $b=$obj->dealData($countries,$provinces);132 echo "<pre>";133 print_r($b);134 echo "</pre>";die;  
135 */136 /*137 #存儲用戶國家省份信息138 $country=‘中國‘;139 $province=‘廣東‘;140 $result=$obj->saveCode(0,$countries,$provinces,$country,$province);141 echo "<pre>";142 print_r($result);143 echo "</pre>";144 */145 /*146 #取出用戶國家省份信息147 $a=$obj->getMessage(15);148 echo "<pre>";149 print_r($a);150 echo "</pre>";die;151 */152 153 ?>

技術分享

測試:

1、dealData處理後的信息,即為’信息表表格’

技術分享

2、saveCode()

userID國家省份
0中國廣東
13日本龜孫子區
15日本王八區

技術分享

3、getMessage()

技術分享技術分享技術分享

參考書籍:

《Redis實戰》 Josiah.Carlson 著

           黃健宏 譯

(以上是自己的一些見解,若有不足或者錯誤的地方請各位指出)


降低Redis內存占用