当前位置: 首页 > Linux

一个内存性能提升的项目实践

时间:2023-04-06 23:58:02 Linux

除C++外的现代开发语言都封装了内存管理,普通开发者无从接触内存的底层操作。更何况各种优秀的开源组件的应用越来越多,比如mysql、redis等,这些甚至不需要你自己开发,直接拿来用就行了。所以也有同学觉得作为应用层开发的同学没有必要去学底层。但是我想通过本文的实际案例告诉大家,即使不直接接触底层的内存操作,也只是使用一些开源的工具。如果你能理解底层的工作原理,你就可以将它们发挥到极致。用户访问历史读写需求如果现在有这样的业务需求,用户每次刷新都需要获取新的数据进行消费,但是不能和之前的访问历史重复。您可以将它与您经常使用的新闻推送应用程序连接起来,例如今日头条。你想每次都看到新的新闻,但你绝对不想看到你过去读过的文章。这样,在实现功能的时候,需要保存用户的访问历史。当用户再次刷新时,必须先获取用户的历史记录,不能重复推送给用户的数据。当推荐完成后,还需要在历史记录中记录新推荐的数据id。为了适当降低实现复杂度,我们可以规定每个用户只需要不重复过去的10000条记录即可。这样一来,每个用户最多只需要保存10000条历史id,如果存储满了,最早的历史记录就会被挤掉。下面进一步说明这个需求的几个关键点:每个dataid是一个int整数,表示每个用户需要保存10000个id,在用户刷新开始时读取所有10000条历史。过滤一次。每次用户刷新结束,需要写入10条新访问的记录。如果超过10000条,就需要挤出最早的记录。用户每访问一次,都会涉及到一个万级的数据。对集合进行一次读取和一次写入操作。好了,需求描述完了,那我们的技术方案怎么设计呢?相信你也能想到很多实现方案。今天我们将比较基于Redis的两种存储方案的性能。方案一:使用Redis列表存储。第一个想到的方法是使用Redislist来存储。因为这种数据结构设计太适合上面的场景了。List下的lrange命令可以实现一次读取用户所有数据id的需求。$redis->lrange('TEST_KEY',0,9999);lpush命令可以实现新数据id的写入,ltrim可以保证用户记录数不超过10000条。$redis->lpush('TEST_KEY',1,2,3,4,5,6,7,8,9,10);$redis->ltrim('TEST_KEY',0,9999);我们准备一个用户,预先保存10000个ID。写的时候每次只写10个新的id,读的时候通过lrange一次性全部读出来。进行性能耗时测试,结果如下。writerepeats:10000timeconsume:0.65939211845398each6.5939211845398E-5Rriterepeats:10000timeconsumed:42.383342027664each0.0042383342027664方案二:使用Redis的字符串直接存储我能想到的另一种技术。我们可以将10000个用int表示的数据id拼接成一个字符串,用特殊字符分隔。例如:“100000_100001_10002”。存储的时候拼接起来,然后把这个大字符串写入Redis。读取时,将大字符串作为一个整体读取,然后用字符切割成数组使用。因为在使用字符串存储时,保存前多了一个字符串拼接的操作,读取后多了一个将字符串拆分成数组的操作。在测试字符串方案时,为了公平起见,我们需要将这两个步骤的开销考虑在内。核心代码如下:$userItems=array(...);//writefor($i=0;$i<$repeats;$i++){$redis->set('TEST_KEY',implode('_',$userItems));}//读取($i=0;$i<10000;$i++){$items=explode("_",$redis->get('TEST_KEY'));}耗时测试结果如下Writerepeats:10000timeconsumed:6.4061808586121each0.00064061808586121Readrepeats:10000timeconsumed:4.9698271751404each0.00049698271751404结论让我们直观地对比一下两种技术方案的性能数据。||写作时间|阅读时间|总时间|----|----|----|----||清单|0.066毫秒|4.238毫秒|4.304毫秒||字符串|0.640毫秒|0.496毫秒|1.136毫秒|在基于列表的方案中,写入速度非常快,只需要0.066ms,因为只需要写入10条新增加的记录,并且增加了一个链表的截断操作,但是读取性能就慢很多,超过4ms。原因之一是因为阅读需要全遍历,其实还有第二个原因。我们案例的数据量太大,所以Redis内部其实是用双端链表实现的。.从上图可以看出,链表是通过指针连接起来的。大量的节点极有可能随机分布在内存的各个位置,这样当你遍历整个链表时,实际上内存很大概率会以随机模式工作。string-basedscheme比list写起来要花更多的时间,因为每次都需要写10000条。但是读取性能比列表提升了10倍,整体耗时仅为方案1的1/4左右。为什么?我们来看看redis字符串数据结构的内存布局。可以看出,如果使用string来存储,无论用户数据id有多少,所有的访问都是顺序IO。顺序IO有两个优点:1.内存顺序IO的耗时仅为随机IO的1/3-1/4左右。2.对于读来说,顺序访问会大大提高CPUL1,L2,L3的缓存命中率,所以如果对内存的工作原理有深刻的理解,即使不能直接操作内存,即使你只用一些开源软件,还是可以发挥出它的最大性能~内功养成与内存练习专辑:1.带你深入理解内存对齐的底层原理2.随机内存访问比顺序访问慢,以及带你深入了解内存IO流程3.从DDR到DDR4,内存核心频率基本没有太大提升4.实测内存中顺序IO和随机IO访问延迟的差异5.揭穿“谎言”内存厂商》,实测内存带宽的实际表现6.NUMA架构下内存访问延迟的差异!7.PHP7内存性能优化精髓8.一个内存性能提升的项目实践9.挑战Redis单实例内存最大极限,“遭遇”NUMA陷阱!我的公众号是“练内功练功”。在这里,我不是简单地介绍技术理论,也不是只介绍实践经验。而是理论联系实际,用实践加深对理论的理解,用理论提高技术实践能力。欢迎关注我的公众号,分享给你的朋友吧~~~