現代的開發語言除了C++之外,大部分都對內存管理作好了封裝,通常的開發者根本都接觸不到內存的底層操做。更況且如今各類優秀的開源組件應用愈來愈多,例如mysql、redis等,這些甚至都不須要你們動手開發,直接拿來用就行了。因此有些同窗也會以爲做爲應用層開發的同窗沒有學習的必要去學習底層。node
但我想經過本文的實際案例告訴你們,哪怕不直接接觸內存底層操做,就只是用一些開源的工具,若是你能理解底層的工做原理,你也可以用到極致。
1用於訪問歷史存儲需求假如如今有這樣一個業務需求,用戶每次刷新都須要得到要消費的新數據,可是不能和以前訪問過的歷史重複。你能夠把它和你常常在用的今日頭條之類的信息流app聯繫起來。每次都要看到新的新聞,可是你確定不想看到過去已經看過的文章。這樣在功能實現的時候,就必要保存用戶的訪問歷史。當用戶再來刷新的時候,首先得獲取用戶的歷史記錄,要保證推給用戶的數據和以前的不重複。當推薦完成的時候,也須要把此次新推薦過的數據id記錄到歷史裏。
mysql
爲了適當下降實現複雜度,咱們能夠規定每一個用戶只要不和過去的一萬條記錄重複就能夠了。這樣每一個用戶最多隻須要保存一萬條歷史id,若是存滿了就把最先的歷史記錄擠掉。咱們進一步具體化一下這個需求的幾個關鍵點:redis
每一個用戶要保存1萬條idsql
每次用戶刷新開始的時候須要將這1萬條歷史所有讀取出來過濾一遍數組
每次用戶刷新結束的時候須要將新訪問過的10條寫入一遍,若是超過1萬需將最先的記錄擠掉數據結構
可見,每次用戶訪問的時候,會涉及到一個1萬規模的數據集上的一次讀取和一次寫入操做。好了,需求描述完了,咱們怎麼樣進行咱們的技術方案的設計呢?相信你也能想到不少實現方案,咱們今天來對比兩個基於Redis下的存儲方案在性能方面的優劣。2Redis方案一:用list存儲 app
首先能想到的第一個辦法就是用Redis的List來保存。由於這個數據結構設計的太適合上面的場景了。List下的lrange命令能夠實現一次性讀取用戶的全部數據id的需求。
ide
$redis->lrange('TEST_KEY', 0,9999);
lpush命令能夠實現新的數據id的寫入,ltrim能夠保證將用戶的記錄數量不超過1萬條。工具
$redis->lpush('TEST_KEY', 1,2,3,4,5,6,7,8,9,10);
$redis->ltrim('TEST_KEY', 0,9999);
咱們準備一個用戶,提早存好一萬條id。寫入的時候每次只寫入10條新的id,讀取的時候經過lrange一次所有讀取出來。進行一下性能耗時測試,結果以下。佈局
Write repeats:10000 time consume:0.65939211845398 each 6.5939211845398E-5
Rrite repeats:10000 time consume:42.383342027664 each 0.0042383342027664
3Redis方案二:用string存儲
我能想到的另一個技術方案就是直接用String來存。咱們能夠把1萬個int表示的數據id拼接成一個字符串,用一個特殊的字符把他們分割開。例如:"100000_100001_10002"這種。存儲的時候,拼接一下,而後把這個大字符串寫到Redis裏。讀取的時候,把大字符串總體讀取出來,而後再用字符切割成數組來使用。
因爲用string存儲的時候,保存前多了一個拼接字符串的操做,讀取後多了一步將字符串分割成數組的操做。在測試string方案的時候,爲了公平起見,咱們把須要把這兩步的開銷也考慮進來。核心代碼以下:
$userItems = array(......);
//寫入
for($i=0; $i<$repeats; $i++){
$redis->set('TEST_KEY', implode('_', $userItems));
}
//讀取
for($i=0; $i<10000; $i++){
$items = explode("_", $redis->get('TEST_KEY'));
}
耗時測試結果以下
Write repeats:10000 time consume:6.4061808586121 each 0.00064061808586121
Read repeats:10000 time consume:4.9698271751404 each 0.00049698271751404
4結論
咱們再直觀對比下兩個技術方案的性能數據。
圖1 方案1和方案二的性能對比
基於list的方案裏,寫入速度很是快,只須要0.066ms,由於僅僅只須要寫入新添加的10條記錄就能夠了,再加一次鏈表的截斷操做,可是讀取性能可就要慢不少了,超過了4ms。緣由之一是由於讀取須要總體遍歷,但其實還有第二個緣由。咱們本案例中的數據量過大,因此Redis在內部其實是用雙端鏈表來實現的。
圖2 Redis之雙端列表內存結構
經過上圖你可能看出來,鏈表是經過指針串起來的。大量的node之間極大多是隨機地分佈在內存的各個位置上,這樣你遍歷整個鏈表的時候,實際上大機率會致使內存的隨機模式下工做。
基於string方案在寫入的時候耗時比list要高,由於每次都得須要將1萬條所有寫入一遍。可是讀取性能卻比list高了10倍,整體上耗時加起來大約只有方案一的1/4左右。爲何?咱們再來看下redis string數據結構的內存佈局
圖3 Redis之string內存結構
可見,若是用string來存儲的話,無論用戶的數據id有多少,訪問將所有都是順序IO。順序IO的好處有兩點:
1. 一內存的順序IO的耗時大約只是隨機IO的1/3-1/4左右,
2. 對於讀取來講,順序訪問將極大地提高CPU的L一、L二、L3的cache命中率
因此若是你深刻了內存的工做原理,哪怕你不能直接去操做內存,即便只是用一些開源的軟件,你也可以將它的性能發揮到極致~