现代的开发语言除了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命中率
因此若是你深刻了内存的工做原理,哪怕你不能直接去操做内存,即便只是用一些开源的软件,你也可以将它的性能发挥到极致~