古代的开发语言除了C++以外,大部分都对内存治理做好了封装,个别的开发者基本都接触不到内存的底层操作。更何况当初各种优良的开源组件利用越来越多,例如mysql、redis等,这些甚至都不须要大家入手开发,间接拿来用就好了。所以有些同学也会感觉作为应用层开发的同学没有学习的必要去学习底层。
但我想通过本文的理论案例通知大家,哪怕不间接接触内存底层操作,就只是用一些开源的工具,如果你能了解底层的工作原理,你也可能用到极致。
用户拜访历史读写需要
如果当初有这样一个业务需要,用户每次刷新都须要取得要生产的新数据,然而不能和之前拜访过的历史反复。你能够把它和你常常在用的今日头条之类的信息流app分割起来。每次都要看到新的新闻,然而你必定不想看到过来曾经看过的文章。 这样在性能实现的时候,就必要保留用户的拜访历史。当用户再来刷新的时候,首先得获取用户的历史记录,要保障推给用户的数据和之前的不反复。当举荐实现的时候,也须要把这次新举荐过的数据id记录到历史里。
为了适当升高实现复杂度,咱们能够规定每个用户只有不和过来的一万条记录反复就能够了。这样每个用户最多只须要保留一万条历史id,如果存满了就把最早的历史记录挤掉。咱们进一步具体化一下这个需要的几个关键点:
- 每个数据id是一个int整数来示意
- 每个用户要保留1万条id
- 每次用户刷新开始的时候须要将这1万条历史全副读取进去过滤一遍
- 每次用户刷新完结的时候须要将新拜访过的10条写入一遍,如果超过1万需将最早的记录挤掉
可见,每次用户拜访的时候,会波及到一个1万规模的数据集上的一次读取和一次写入操作。
好了,需要形容完了,咱们怎么样进行咱们的技术计划的设计呢?置信你也能想到很多实现计划,咱们明天来比照两个基于Redis下的存储计划在性能方面的优劣。
计划一:用Redis的list来存储
首先能想到的第一个方法就是用Redis的List来保留。因为这个数据结构设计的太适宜下面的场景了。
List下的lrange命令能够实现一次性读取用户的所有数据id的需要。
$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-5Rrite repeats:10000 time consume:42.383342027664 each 0.0042383342027664
计划二:用Redis的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.00064061808586121Read repeats:10000 time consume:4.9698271751404 each 0.00049698271751404
论断
咱们再直观比照下两个技术计划的性能数据。
| | 写入耗时 | 读取耗时 | 总耗时
| ---- | ---- | ---- | ---- |
| list | 0.066ms | 4.238ms | 4.304ms|
| string | 0.640ms | 0.496ms | 1.136ms|
基于list的计划里,写入速度十分快,只须要0.066ms,因为仅仅只须要写入新增加的10条记录就能够了,再加一次链表的截断操作,然而读取性能可就要慢很多了,超过了4ms。起因之一是因为读取须要整体遍历,但其实还有第二个起因。咱们本案例中的数据量过大,所以Redis在外部实际上是用双端链表来实现的。.
通过上图你可能看进去,链表是通过指针串起来的。大量的node之间极大可能是随机地散布在内存的各个地位上,这样你遍历整个链表的时候,实际上大概率会导致内存的随机模式下工作。
基于string计划在写入的时候耗时比list要高,因为每次都得须要将1万条全副写入一遍。然而读取性能却比list高了10倍,总体上耗时加起来大概只有计划一的1/4左右。为什么?咱们再来看下redis string数据结构的内存布局
可见,如果用string来存储的话,不论用户的数据id有多少,拜访将全部都是程序IO。程序IO的益处有两点:
- 1.一内存的程序IO的耗时大概只是随机IO的1/3-1/4左右,
- 2.对于读取来说,程序拜访将极大地晋升CPU的L1、L2、L3的cache命中率
所以如果你深刻了内存的工作原理,哪怕你不能间接去操作内存,即便只是用一些开源的软件,你也可能将它的性能施展到极致~
开发内功修炼之内存篇专辑:
- 1.带你深刻了解内存对齐最底层原理
- 2.内存随机也比程序拜访慢,带你深刻了解内存IO过程
- 3.从DDR到DDR4,内存外围频率其实基本上就没太大的提高
- 4.理论测试内存在程序IO和随机IO时的拜访延时差别
- 5.揭穿内存厂家“谎话”,实测内存带宽实在体现
- 6.NUMA架构下的内存拜访提早区别!
- 7.PHP7内存性能优化的思维精华
- 8.一次内存性能晋升的我的项目实际
- 9.挑战Redis单实例内存最大极限,“遭逢”NUMA陷阱!
我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术实践,也不只介绍实践经验。而是把实践与实际联合起来,用实际加深对实践的了解、用实践进步你的技术实际能力。欢送你来关注我的公众号,也请分享给你的好友~~~