目次
一. 背景
二. 引发思考的根源
三. 独立缓存层的设计
四. 程序实现
五. 小结
ホームページ php教程 php手册 服务端需要一个新的分层 -“数据缓存层”

服务端需要一个新的分层 -“数据缓存层”

Jun 06, 2016 pm 08:11 PM
層状の 存在する 成し遂げる 仕える 読む 必要

— 实现读取缓存在数据访问层中抽离策略 一. 背景 使用数据缓存在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 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

Huawei 携帯電話にデュアル WeChat ログインを実装するにはどうすればよいですか? Huawei 携帯電話にデュアル WeChat ログインを実装するにはどうすればよいですか? Mar 24, 2024 am 11:27 AM

Huawei 携帯電話にデュアル WeChat ログインを実装するにはどうすればよいですか?ソーシャルメディアの台頭により、WeChatは人々の日常生活に欠かせないコミュニケーションツールの1つになりました。ただし、多くの人は、同じ携帯電話で同時に複数の WeChat アカウントにログインするという問題に遭遇する可能性があります。 Huawei 社の携帯電話ユーザーにとって、WeChat の二重ログインを実現することは難しくありませんが、この記事では Huawei 社の携帯電話で WeChat の二重ログインを実現する方法を紹介します。まず第一に、ファーウェイの携帯電話に付属するEMUIシステムは、デュアルアプリケーションを開くという非常に便利な機能を提供します。アプリケーションのデュアルオープン機能により、ユーザーは同時に

PHP プログラミング ガイド: フィボナッチ数列を実装する方法 PHP プログラミング ガイド: フィボナッチ数列を実装する方法 Mar 20, 2024 pm 04:54 PM

プログラミング言語 PHP は、さまざまなプログラミング ロジックやアルゴリズムをサポートできる、Web 開発用の強力なツールです。その中でも、フィボナッチ数列の実装は、一般的で古典的なプログラミングの問題です。この記事では、PHP プログラミング言語を使用してフィボナッチ数列を実装する方法を、具体的なコード例を添付して紹介します。フィボナッチ数列は、次のように定義される数学的数列です。数列の最初と 2 番目の要素は 1 で、3 番目の要素以降、各要素の値は前の 2 つの要素の合計に等しくなります。シーケンスの最初のいくつかの要素

Huawei携帯電話にWeChatクローン機能を実装する方法 Huawei携帯電話にWeChatクローン機能を実装する方法 Mar 24, 2024 pm 06:03 PM

Huawei 携帯電話に WeChat クローン機能を実装する方法 ソーシャル ソフトウェアの人気と人々のプライバシーとセキュリティの重視に伴い、WeChat クローン機能は徐々に人々の注目を集めるようになりました。 WeChat クローン機能を使用すると、ユーザーは同じ携帯電話で複数の WeChat アカウントに同時にログインできるため、管理と使用が容易になります。 Huawei携帯電話にWeChatクローン機能を実装するのは難しくなく、次の手順に従うだけです。ステップ 1: 携帯電話システムのバージョンと WeChat のバージョンが要件を満たしていることを確認する まず、Huawei 携帯電話システムのバージョンと WeChat アプリが最新バージョンに更新されていることを確認します。

Linux でサービスを再起動する正しい方法は何ですか? Linux でサービスを再起動する正しい方法は何ですか? Mar 15, 2024 am 09:09 AM

Linux でサービスを再起動する正しい方法は何ですか? Linux システムを使用していると、特定のサービスを再起動する必要がある状況がよく発生しますが、サービスの再起動時に実際にサービスが停止しない、または開始しないなどの問題が発生することがあります。したがって、サービスを再起動する正しい方法を習得することが非常に重要です。 Linux では、通常、systemctl コマンドを使用してシステム サービスを管理できます。 systemctl コマンドは systemd システム マネージャーの一部です

Golangで正確な除算演算を実装する方法 Golangで正確な除算演算を実装する方法 Feb 20, 2024 pm 10:51 PM

Golang で正確な除算演算を実装することは、特に財務計算を含むシナリオや高精度の計算が必要なその他のシナリオでよくあるニーズです。 Golang の組み込みの除算演算子「/」は浮動小数点数に対して計算されるため、精度が失われる場合があります。この問題を解決するには、サードパーティのライブラリまたはカスタム関数を使用して、正確な除算演算を実装します。一般的なアプローチは、math/big パッケージの Rat タイプを使用することです。これは分数の表現を提供し、正確な除算演算を実装するために使用できます。

Golang がゲーム開発の可能性を可能にする方法をマスターする Golang がゲーム開発の可能性を可能にする方法をマスターする Mar 16, 2024 pm 12:57 PM

今日のソフトウェア開発分野では、効率的で簡潔かつ同時実行性の高いプログラミング言語として、Golang (Go 言語) が開発者にますます好まれています。豊富な標準ライブラリと効率的な同時実行機能により、ゲーム開発の分野で注目を集めています。この記事では、ゲーム開発に Golang を使用する方法を検討し、具体的なコード例を通じてその強力な可能性を示します。 1. ゲーム開発における Golang の利点 Golang は静的型付け言語として、大規模なゲーム システムの構築に使用されます。

PHP ゲーム要件実装ガイド PHP ゲーム要件実装ガイド Mar 11, 2024 am 08:45 AM

PHP ゲーム要件実装ガイド インターネットの普及と発展に伴い、Web ゲーム市場の人気はますます高まっています。多くの開発者は、PHP 言語を使用して独自の Web ゲームを開発することを望んでおり、ゲーム要件の実装は重要なステップです。この記事では、PHP 言語を使用して一般的なゲーム要件を実装する方法を紹介し、具体的なコード例を示します。 1. ゲームキャラクターの作成 Web ゲームにおいて、ゲームキャラクターは非常に重要な要素です。ゲームキャラクターの名前、レベル、経験値などの属性を定義し、これらを操作するメソッドを提供する必要があります。

Ubuntu PHP サービスが正常に起動しない場合の解決策 Ubuntu PHP サービスが正常に起動しない場合の解決策 Feb 28, 2024 am 10:48 AM

タイトル: Ubuntu で PHP サービスが正常に起動できない問題を解決する方法と具体的なコード例 Ubuntu を使用して Web サイトやアプリケーションを構築する場合、PHP サービスが正常に起動できず、Web サイトが起動できないという問題がよく発生します。正常にアクセスできなくなるか、アプリケーションが正常に機能できなくなります。この記事では、Ubuntu で PHP サービスが正常に開始できない問題を解決する方法を紹介し、読者がそのような問題をすぐに解決できるように具体的なコード例を示します。 1. PHP 設定ファイルを確認する まず、PHP 設定ファイルを確認する必要があります。

See all articles