PHP系统的服务器端内存缓存架构的分析和优化

来源:互联网 发布:西华师范大学知乎 编辑:程序博客网 时间:2024/05/22 17:33


缓存体系

为了提交服务器的性能和减小对数据库的压力,缓存在宝贝项目中应用较多,基本上很少有直接对数据库的访问,绝大多数的数据均来自于缓存系统。所以缓存系统的整体结构和实现效率相对来说比较重要。

类别

    memcache:对于由用户创建的信息,均采用memcache缓存。其特点是缓存键值中有_1234(userid)的字样(memcache服务器是一种高效的分布式内存缓存服务器,可以方便的进行扩展,“缺点”是数据是通过网络访问,有一定的网络开销,这种网络开销甚至有时候会成为系统的瓶颈!)

    apc:对于所有系统配置性的数据采用apc缓存。APC缓存就存放于web机自身,为了避免过多的占用WEB机内存,一般情况下都只会存放一些访问频率很高的数据。()

分析

采用apc之前有考虑过在web机架设memcache服务器,但最终放弃这个方案。原因如下

如果在web机上架设memcache服务,那一台机器上提供了两个服务 web 和 memcaced,当然这样做也没关系,只是隐隐觉得不太好。毕竟我们向往的架构还是各司其职,以便于分析性能和瓶颈,提高整体的效率。

memcache架设在web机上,php访问memcache 是否就相当于访问本机内存呢。答案是no,即使在web上架设memcache,PHP的访问依然是通过网络进行的,memcache有提供一个sock的连接方式用于本机连接,但网上查到的文章说性能很不理想。memcache是一个分布式的高性能的缓存方案,但他不是全能的方案,他被设计的目的就是为了提供分布式的内存缓存,现在我们要将他做为一个本地缓存方案本身就是强人所难。

APC本身最大的功能并不是提供内存缓存,主要是提供对PHP编译过程中OPCODE缓存的支持,能极大的提高PHP在编译阶段的效率。他同时也提供了内存缓存的功能。并且内存缓存的效率很高,曾经在网上看过一篇文章说访问APC缓存的速度与访问PHP自身内存数据的速度差不多。因此这是一个很高效的缓存 机制,并且APC存放于WEB,对他的访问没有网络开销。这是他与MEMCACHE相比的最大特性。

PHP结构

新增了两个配置文件,用于存放apc缓存和memcache缓存的配置。(过期时间配置),如下。

    $config['usercache']['userOneInfo_']['time']='1000';

    $config['usercache']['userOneInfo_']['cache_type']='2';

    $config['usercache']['userOneInfo_']['descript']='单个用户缓存';

可以方便的在配置中对缓存时间进行调整。避免后期再改对程序。

实际代码中的缓存结构。

相对很简单的代码

 

<?php

class user{

    public function getInfo(){

        $this->load->library('cache');

        $cacheName = 'allWorkList';

        $list = array();

        $list = $this->cache->apc_get($cacheName);

        if (!$list) {

            $sql = 'SELECT * FROM b**;

            $query = $this->db['default']->query($sql);

            while ($row = $this->db['default']->fetch_array($query)) {

                $list[$row['work_id']] = $row;

            }

            $this->load->config('syscache');

            $this->cache->apc_set($cacheName, $list, $this->config->config['syscache']['allWorkList']['time']);

        }

        return emtpy($list) ? array() : $list;

}

private function delInfoCache(){

    $this->load->library('cache');

    $cacheName = 'allWorkList';

    return $this->cache->del_apc($cacheName);

}

public function updateSomething()

{

    $sql = "update work set ....";

    if($this->db['default']->query($sql)) {

        $this->delInfoCache();

    }

    return true;

}

}

?>

如果有缓存则读缓存,如果没有缓存则取数据并放入缓存。 如果有更新或者删除等操作时则需删除缓存(删除即更新缓存),其加需注意

删除缓存的操作与创建缓存的操作在同一个类中,并且为private方法,即只能在本类中创建,更新。该类的对像被调用时外部是不知道其中有缓存的。(这类似于一个原则,用于防止在代码中随意更新缓存造成代码混乱)

上面显示的是apc缓存的访用,memcache访问类似,其实可以构造cache类让他根据配置来使用不同的缓存方案(file,memcache,apc)。

不足与优化

memcache是分布式缓存,所有的数据均请求自网络。有下面一个应用。

for($i=0; $I<100; $i++ ) {

       $this->user->getInfo();

}

这在实际的代码中是经常出现的。或者有时候一次PHP请求中会多次调用到getInfo方法。如果是采用的memcache缓存则会出现多次请求网络取回相同的数据的情况。这当然是很浪费的。尤其是在所取的数据非常大的情况下。我们需要对这种情况进行处理。

改造getInfo方法

public function getInfo(){

    static $list;

    if(!empty($list)) return $list;

    $this->load->library('cache');

    $cacheName = 'allWorkList';

    $list = array();

    $list = $this->cache->apc_get($cacheName);

    if (!$list) {

       $sql = 'SELECT * FROM b**;

        $query = $this->db['default']->query($sql);

        while ($row = $this->db['default']->fetch_array($query)) {

           $list[$row['work_id']] = $row;

        }

       $this->load->config('syscache');

        $this->cache->apc_set($cacheName, $list, $this->config->config['syscache']['allWorkList']['time']);

    }

    return emtpy($list) ? array() : $list;

}

        改动在上面代码中黑色标出的部份,即申明一个全局的静态变量存放从缓存中取回的数据,静态变量在函数调用完成之后并不会马上销毁,而是等PHP请求结束才会回收。在PHP执行过程中如果多次调用到getInfo方法,他会从静态变量中取出数据并立即返回,不会发起网络请求了。

         这样的改动虽然达到了我们的要求但是在实际中增加了很大的代码开销。每次使用缓存都需要单独设置静态变量并进行管理。如果可以我们可以改造cache类,将上面的操作在cache中进行,外部调用即不用做这些改变。也可以从cache类中继承一个子类实现相应的功能。

        实施的具体缓存方案。

        单条记录和列表记录的关系

        有这样的情况,用户的服装列表,数据量相对较大,而且因为访问好友家也会要显示用户宠物的着装情况。(这个还是比较频繁)。因此我们需要在memcache中缓存一个数组如下$testArr。

array(

[123] =>array(

dress_id=>123

dressmod_id=>12

name=>'test'

....

)

[124]=>array(

...

)

)

方法大致如下

public function getUserAllDressList(){

       $sql = 'select ...';

       return $list;//查数据库存入列表缓存,数据如上面的array;

}

上面的返回数组 键值即为id 。但在很多时候我们要对物品进行操作的时候需要先取出这个物品的数据然后进行相关的验证。(防止别人非法穿上了别人的衣服)因此我们需要一个getone的方法,有两种方案如下。

function getOneDressDetail($dress_id) {

      $sql = 'select ...';

       ...

       return $sql;//即为单个服装再建立一个缓存。

}

这种方案相当于在缓存中人工进行了冗余,因为单条记录和数据和列表中的数据其实是一致的。这样子在更新缓存时需要对更新单条记录的缓存也需要更新列表的缓存。因些不是一个太好的方案,但是如果对列表的缓存更新机制做一定的处理其实也未偿不可以应用。比如列表的缓存不主动更新(列表读取的机会很少)

function getOneDressDetail($userId, $dress_id){

      $userAllDressList = $this->getUserAllDressList($userId);//缓存用户所有服装的方法,结果如上面的那个数组

      return $userAllDressList[$dress_id];//这里一般需要进行非空的判断否则可能会报错。

}

        这样就取到了用户的单个服装。并没有去查数据库也没有为单个衣服建立一个单独的缓存。

        这样的方式比较灵活,代码量较少,对于对单件衣服的操作相对较少的环境应该比较适合。但这种方式也有缺点,比如用户穿上了某件衣服,就需要更新用户的缓存,我们这里的单件缓存是取自于列表数据,因些我们需要更新整个列表的数据。国为某件衣服的一个小改变就更新整个列表听起来似乎是很不合适。并且这样的方式还有一个比较大的问题就是我只需要取一条记录,但是会把所有的列表都从缓存服务器中取出。$userAllDressList = $this->getUserAllDressList($userId);这句是取出了整个缓存记录然后由PHP选择其中的一条。对于网络流量的开销也还是比较大。

         于是又有另外一种方案。

         这种方案的应用发生了一点变化。

         用户用户多个宠物,初始化时需一齐获取所有的宠物数据。并进行相应的操作,比如完成打工,打工,培训等等。同上面的情况类似我们也需要对宠物的操作进行验证,也就是要取得单只宠物的数据。宠物数据相对服装数据不同,更新非常频繁,如果每次更新都重建宠物列表的缓存有点不划算。于是我想出了下面一个解决方式。

列表数据分成两部分

public function getPetList($userId){

    $allPetIdArr = $this->getPetIds($userId);

    foreach($allPetIdArr as $v) {

        $list[$v] = $this->getOnePetDetail($v);

    }

    return $list;

}

private function getPetIds($userId)

{

...

return $array;//array(1,2,3)//从缓存中取出用户所有宠物的id

}

private function getOnePetDetail($petId){

...//从缓存中取出单只宠物的数据。

}

        即建立一个IDS的缓存数据表存放用户所有宠物的id. 宠物id缓存的更新只有在购买宠物时会触发,相对非常少,建立后基本不需要更新。

       通过循环去单独取每个宠物的数据,并整合成列表数据。 宠物数据更新时只需要更新单只宠物的数据,不需要重建列表的缓存。

       

       以上的三种方案根据不同的数据更新频率来使用,应该会有一定的提升。不过第三种方案相对更为复杂一些。也许并不是一个很好的模式,而且在建立列表缓存的时候有一个循环,会多次连接数据库。我也并没有做过性能的测试,因些只是一个解决的思路,使用中还需要再根据情况再定,不过对于单条记录更新较频繁的记录这样的方式应该是比较可行的。 *

缓存全局更新

         宝贝1时有发现这样的情况,数据库配置更新或者程序更新,需要更新缓存(系统的和用户的),当时清空了整个memcache服务器。然后用户访问时会重建缓存。在用户访问量较高时可能造成很多请求同时发送到数据库,造成数据库的瞬间压力。有同事(JJC)给了解决方案是记录在线的用户表(宝贝本身就有维护在线表),并且利用脚本清除在线用户的缓存数据,应该是可行的,可以用脚本分几分钟来清除缓存。比如有在线用户1000人,则1分钟清除一次缓存。一次100人,到清除完成为止,这样更新的请求会保持在一个合理的范围内,不会造成数据库峰值过大。






0 0
原创粉丝点击