搜尋
首頁php教程php手册服务端需要一个新的分层 -“数据缓存层”

— 实现读取缓存在数据访问层中抽离策略 一. 背景 使用数据缓存在WEB工程中是一个非常有意义的策略,不仅仅可以减少数据库负载,而且当数据缓存在内存中,能大大提高了的读取速度。 在WEB设计中,我们往往忽略一个重要的信息 – Key,每当我们需要缓存的时候

— 实现读取缓存在数据访问层中抽离策略

一. 背景

使用数据缓存在WEB工程中是一个非常有意义的策略,不仅仅可以减少数据库负载,而且当数据缓存在内存中,能大大提高了的读取速度。

在WEB设计中,我们往往忽略一个重要的信息 – Key,每当我们需要缓存的时候,只是单纯的对一个Key进行SET或者GET操作,时间长了,也许就出现了以下的问题:

  • 这个数据缓存的Key是什么;
  • 这个数据缓存生命周期有多长;
  • 如何不获取这个数据缓存,而得到实际的数据做测试;
  • 在其他项目中同时可能用到这个数据缓存,如何重置这个缓存
  • ……

这就造成了我们平时常见的一种场景,一位同事查到原因,高喊:

“Shit!为什么生产环境上的数据总是测试有问题,测试环境都好好的,线上一定是有缓存,但是我们怎么把它清除掉!

 

二. 引发思考的根源

先来看一个简单的例子:获取服务器的数据列表

// 这是一个读取服务器相关信息的数据访问类
Class ServerStore {
    // 根据数量获取最新的测试服务器列表数据
    public function fetchTestingList($limit);
}

看到这个类基本所有人都知道fetchTestingList的方法体要这么写:

// 这是一个读取服务器相关信息的数据访问类
Class ServerStore {
    // 根据数量获取最新的测试服务器列表数据
    public function fetchTestingList($limit) {
        // OK,开始找Cache
        $cache_key = "ServerTestingList:{$limit}";
        if ( FALSE !== $data_from_cache = Cache::getInstance()->get($cache_key) ) {
            return $data_from_cache;
        }
        // Cache没找到,找数据表吧
        $data = $this->db->select('id','name','time')
            ->from('server')
            ->limit($limit)
            ->fetchRow();
        // 写缓存,给个600秒意思下吧
        Cache::getInstance()->set($cache_key, $data, 600);
        return $data;
    }
}

现在我们调用这个方法获取数据:

$ServerStore = new ServeStore();
$list = $ServerStore->fetchTestingList(4);

这个对象我们统一称之为Store(数据访问) 对象

看似习以为常的方法,却隐藏的几个重要的问题:

  • 这个缓存周期TTL,600秒不是我需要的,那把这个作为一个参数吧。
  • 这个方法我不希望得到缓存数据,想要返回实际数据表读取的数据,那么把是否需要缓存作为一个参数吧,到此已经三个参数。
  • 有些方法体的参数个数比较多,而且参数还有数组格式,那么组合的Cache Key不是需要在这里构造规则?
  • 我这个ServerStore有10个方法体,我难道要写10次Cache::getInstance?我还有5个Store层呢,这是在坑爹吗?
  • 我的后台项目中有个需求需要清空这个缓存,要是前台被人偷偷修改了Key的规则,我怎么拿到这个Key!这是坑到家了吗?

这个例子确实是开了一个玩笑,但事实上我们的生产环境上经常出现这样的问题,并不是因为我们的程序逻辑出错了,而是由于我们在一开始设计上就没有考虑到扩展性。

三. 独立缓存层的设计

为什么我们将缓存写在Store对象内,如果写在Store对象外头可以吗?形如这般:

$ServerStore->ttl(600)->fetchTestingList(4);
$ServerStore->nocache()->fetchTestingList(4);

那么这应该不是一个Store对象可以做到方式,我们不妨将它叫做Service(数据服务)对象:

$ServerService->ttl(600)->fetchTestingList(4);
$ServerService->nocache()->fetchTestingList(4);

我们可以猜想到:

  • Service对象中ttl()方法是应该用来返回一个Cache(数据缓存)对象,通过Cache对象的fetchTestingList()方法获取数据。
  • Service对象中nocache()方法则是用来返回一个之前的Store对象,通过Store对象的fetchTestingList()方法获取数据。
  • Service对象中必然存在两个属性不妨叫做$_db_handler, $_cache_handler用来切换对应操作访问对象。
  • Cache对象中只用于返回Cache中的值。
  • Service对象中使用ttl方法获取不到Cache时(TTL失效),必然还要自动获取Store对象进行读取最新的数据。

根据我们的猜想,画出UML图:

为了保证通用性我们将CacheService都继承一个抽象基类:

一切看似逻辑没什么异常了,可是我们Key的组合怎么处理呢?怎么创建这个Key?

先来处理第一个问题,从一开始的设计方案看,ServerStore中每个方法只会存在一个Key,也就是说这个Store中有多少个方法体,对应的就应该有多少个Key。

为此我们可以建立一个ServerCacheKey(数据缓存键)对象,里面的每个方法都应该匹配ServerStore中对应的方法。那么我们不妨设计一个接口iServer,然而ServerCacheKey,ServerCache以及ServerStore实现这个接口,通过这样的实现ServerCacheKey将也有fetchTodayTestingList这个方法,自然能通过对应的参数组合出需要的Key。

?????? 那么第二个问题,创建这个Key的工作可能出现的位置在:

  1. Cache对象:要通过创建Key来获取缓存中对应Key的Value;
  2. Service对象:当使用Cache对象获取缓存的数据失效时,会重新使用Store对象从数据表中查出新的值,需要重新通过创建Key的操作重新将新数据写入缓存。

有了接口的帮助,那么实现接口的类,它们的类名,方法,参数就被统一了,为此我们再建立一个CacheKeyCreater类通过这一些列的规则就可以创建指定的Key。

注:ServerCache可以继承抽象类Call魔术自动通过CacheKeyCreater实现的接口方法,进行读取数据缓存方法,因此我们这里移除ServerCache实现iServer的过程。

四. 程序实现

为了方便程序实现和调试,以下使用ThinkPHP框架模拟程序,将App下Lib目录结构做如下规划:

/Lib/Core:核心类文件,CacheKeyCreater, BaseService, BaseCache等放置处

/Lib/Interface: 接口类放置处

/Lib/Service: 服务类放置处

/Lib/Store: 数据访问类放置处

/Lib/Cache: 数据缓存类放置处

/Lib/CacheKey: 数据缓存键类放置处

使用ThinkPHP自带的D函数自动装载对应目录下类库,部分类库需要增加自定义的autoload功能。

  1. 接口类iServer:
    // 服务器列表接口
    Interface iServer {
        // 获取今日开服列表
        public function fetchTodayOpeningList($limit);
        // 获取今日测试列表
        public function fetchTodayTestingList($limit);
    }
  2. CacheKeyCreater 创建Key类:
    /**
     * APP Cache Key生成类
     *
     * @category Core
     * @package  Core
     * @author   Lancer <lancer.he>
     * @since    2014-03-06
     */
    Final Class CacheKeyCreater {
        public static function create($class, $func, $args) {
            $cachekey_class = $class . 'CacheKey';
            return call_user_func_array(array($cachekey_class, $func), $args);
        }
    }</lancer.he>
  3. Cache抽象基类BaseCache:
    /**
     * APP Cache层抽象基类
     *
     * @category Core
     * @package  Core
     * @author   Lancer <lancer.he>
     * @since    2014-03-06
     */
    Abstract Class BaseCache {
        public function __call($func, $args) {
            $class = str_replace('Cache', '', get_class($this) );
            $key = CacheKeyCreater::create($class, $func, $args);
            return Cache::getInstance()->get($key);
        }
    }</lancer.he>
  4. Service抽象基类BaseService:
    /**
     * APP Service层抽象基类,自动选择Cache层/Store层
     *
     * @category Core
     * @package  Core
     * @author   Lancer <lancer.he>
     * @since    2014-03-06
     */
    Abstract Class BaseService {
        protected $_cache_handler;
        protected $_db_handler;
        protected $_class_name;
        protected $_layer_cache_tag = 'Cache';//Cache类放在Cache目录下
        protected $_layer_store_tag = 'Store';//Store类放在Store对象下
        public function __construct() {
            $this->_class_name    = str_replace('Service', '', get_class($this) );
            $this->_cache_handler = D($this->_class_name, $this->_layer_cache_tag);
            $this->_db_handler    = D($this->_class_name, $this->_layer_store_tag);
        }
        public function ttl($ttl) {
            $this->_cache_handler->ttl = $ttl;
            return $this;
        }
        public function nocache() {
            return $this->_db_handler;
        }
        public function __call($func, $args) {
            $data_from_cache = call_user_func_array(array($this->_cache_handler, $func), $args);
            if ( FALSE !== $data_from_cache ) 
                return $data_from_cache;
            $data = call_user_func_array(array($this->_db_handler, $func), $args);
            $key  = CacheKeyCreater::create($this->_class_name, $func, $args);
            Cache::getInstance()->set($key, $data, $this->_cache_handler->ttl );
            return $data;
        }
    }</lancer.he>
  5. ServeStore:必须实现接口iServer的方法
    /**
     * 服务器数据读取层
     *
     * @category Cache
     * @package  Core
     * @author   Lancer <lancer.he>
     * @since    2014-03-02
     */
    Class ServerStore implements iServer{
        public function fetchTodayOpeningList($limit=4) {
            $condition  = array('status' => 1, 'audit' => 1 'opendate' => date('Y-m-d') );
            return D('Server')->order('opentime DESC')
                ->where($condition)
                ->limit($limit);
                ->getField('id, game_id, name, opentime, statusval');
        }
        public function fetchTodayTestingList($limit=4) {
            $condition  = array('status' => 0, 'audit' => 1 'opendate' => date('Y-m-d') );
            $pagination = array('limit' => 4);
            return D('Server')->order('opentime DESC')
                ->where($condition)
                ->limit($limit);
                ->getField('id, game_id, name, opentime, statusval');
        }
    }</lancer.he>
  6. ServerCacheKey:必须实现接口iServer的方法
    /**
     * 服务器列表缓存键类
     *
     * @category CacheKey
     * @package  Extend
     * @author   Lancer <lancer.he>
     * @since    2014-03-02
     */
    Class ServerCacheKey implements iServer {
        public function fetchTodayOpeningList($limit) {
            return 'ServerTodayOpeningList:' . $limit;
        }
        public function fetchTodayTestingList($limit) {
            return 'ServerTodayTestingList:' . $limit;
        }
    }</lancer.he>
  7. ServerService / ServerCache 由于集成父类,目前暂时是空的:
    Class ServerCache extends BaseCache {}
    Class ServerService extends BaseService {}

测试过程:将设计的程序,我们大胆的在Action下做一个测试:

Class TestAction extends BaseAction {
    public function cachetest() {
        D('Server', 'Service')->ttl(600)->fetchTodayTestingList(4);
        D('Server', 'Service')->nocache()->fetchTodayOpeningList(4);
        $this->show(' ');
    }
}

测试结果:访问页面后在Memadmin查询:

测试结果:利用ThinkPHP自带的追踪SQL记录来查看下效果:

确实只有一条SQL查询,证明我们的设计和程序逻辑是没有问题。

五. 小结

通过设计模式分离出一个数据缓存层,解决了以下的问题:

  1. 其他关联模块中可以通过CacheKeyCreater的方式直接获取到Key进行操作处理;
  2. 单元测试时可以自主选择使用缓存层或是数据层进行测试;
  3. 通过接口的规数据访问层和数据缓存层的方法,一旦修改出错,必然抛出致命错误,提高开发人员在项目初期对接口进行规范设计的技能,减少对数据层的修改。
陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具