CodeIgniter源碼分析8 - 緩存

這節(jié)看下 CI 提供的緩存功能,緩存也是以驅(qū)動的方式運行的,由如下幾部分組成

  • Cache_apc.php:提供對 php 字節(jié)碼 opcode 和用戶數(shù)據(jù)的緩存。
  • Cache_dummy.php:實現(xiàn)了驅(qū)動接口的一個空類,僅僅是為了防止某個驅(qū)動不存在時代碼報錯。
  • Cache_file.php:文件緩存。
  • Cache_memcached.php:memcache 緩存
  • Cahce_redis.php:redis 緩存,不過只是封裝一些基本功能。
  • Cache_wincache.php :提供 Windows 上的緩存功能。
image.png

關(guān)于 APC

apc 全稱 alternative php cache(可選 php 緩存),其提供對 opcode用戶數(shù)據(jù) 的緩存!

什么是 opcode?

opcode 是 php 腳本編程生成的機器碼,一個 php 腳本的解析過程大致如下

image.png

正如你看到的,如果我們能將 Parse 和 Compile 這兩步能省掉的話,直接得到機器碼對于提升 php 腳本的執(zhí)行性能是非常有幫助的,于是在早期 php 版本中出現(xiàn)了提供 Opcode 緩存的擴展 apc!

image.png

但是由于該擴展在 php 5.5.* 有嚴重的內(nèi)存問題,就被廢棄了,然后官方出了一個新的 Opcode 緩存 Zend Opcache,在 php 5.5 以后的版本中自帶了該擴展!

apc 除了提供 Opcode 緩存,還提供用戶數(shù)據(jù)緩存,既然 apc 被廢了,于是后面就出了一個替代 apc 的擴展 acpu, 該擴展只被用作緩存用戶數(shù)據(jù),其接口完全兼容 apc!

那什么是用戶數(shù)據(jù)緩存呢?

用戶數(shù)據(jù)緩存就是編寫 PHP 代碼時用 apc_store 和 apc_fetch 函數(shù)操作讀取、寫入的數(shù)據(jù);

apcu 提供的緩存可以看做是一個輕量級 key/value 存儲系統(tǒng),類似 memcache,如果你緩存的用戶數(shù)據(jù)量大的話,還是建議你使用 memcache,redis 等專業(yè)緩存系統(tǒng)!



好了,說完基本概念,現(xiàn)在讓我們寫下這樣一段緩存代碼

 $this->load->driver('cache', array('adapter' => 'apc', 'backup' => 'file'));

if ( ! $foo = $this->cache->get('foo'))
{
    echo 'Saving to the cache!<br />';
    $foo = 'foobarbaz!';
    $this->cache->save('foo', $foo, 300);
}
echo $foo;

那它載入驅(qū)動后的運行流行是怎么樣的呢?我們的源碼分析就以此展開!


驅(qū)動抽象類加載

之前分析加載器源碼時我們知道 CI 任何資源的載入都是通過加載器實現(xiàn)的,而緩存驅(qū)動是通過加載器中的 driver() 函數(shù)載入的。

注意:driver() 方法載入的是驅(qū)動抽象類!

driver()

public function driver($library, $params = NULL, $object_name = NULL)
{
        //如果加載了多個驅(qū)動,遞歸處理
        if (is_array($library))
        {
            foreach ($library as $driver)
            {
                $this->driver($driver);
            }

            return $this;
        }
        elseif (empty($library))
        {
            return FALSE;
        }

        // 該文件中實有兩個類,分別為驅(qū)動抽象層父類和驅(qū)動器的基類,我們后面會分析這個文件的源碼
        if ( ! class_exists('CI_Driver_Library', FALSE))
        {
            // We aren't instantiating an object here, just making the base class available
            require BASEPATH.'libraries/Driver.php';
        }

        // 這里可以看到,CI 默認驅(qū)動的的架構(gòu)是 $driver/$driver,也就是說如果我們傳入的驅(qū)動是 cache,
        // 那么解析到的驅(qū)動是 Cache/cache,而之前在加載器源碼分析中我們知道在方法 library() 
        // 內(nèi)部會從指定的加載目錄($path)中去查找這個驅(qū)動,最后會實例化該驅(qū)動;
        if ( ! strpos($library, '/'))
        {
            $library = ucfirst($library).'/'.$library;
        }

        return $this->library($library, $params, $object_name);
}

通過上面的源碼看到最終的驅(qū)動是通過加載器的 library() 方法載入的,并在該方法內(nèi)部會實例化 Cache 對象, 這樣整個驅(qū)動抽象層的載入就完成了!

那么驅(qū)動抽象類實例化時到底做了呢?

驅(qū)動抽象類的繼承

驅(qū)動抽象類位于 /libraries/Cache/Cache.php 中,進入該文件首先看到其繼承了一個類 CI_Driver_Library!

image.png

那這個 CI_Driver_Library 類是從哪里來的呢?先留個疑問!

接著往下看,我們觀察 CI_Cache 的構(gòu)造方法。

驅(qū)動抽象類的構(gòu)造函數(shù)

    // $config 參數(shù)就是 $this->load->driver('cache', array('adapter' => 'apc', 'backup' => 'file')) 時的傳入的第二個的參數(shù),
    // adapter 是驅(qū)動器(file,redis,memcache等),backup 是備用的驅(qū)動器,不清楚的話看文檔:
    // https://codeigniter.org.cn/user_guide/libraries/caching.html#id2
    public function __construct($config = array())
    {
        $default_config = array(
            'adapter',
            'memcached'
        );

        // 將 $config 中的配置項掛到當前類的屬性上,由于當前類的屬性有個前綴,所以給加了個 _
        foreach ($default_config as $key)
        {
            if (isset($config[$key]))
            {
                $param = '_'.$key;

                $this->{$param} = $config[$key];
            }
        }

        //檢測是否給緩存配置了前綴,主要是為了防止多個應用的命名沖突
        isset($config['key_prefix']) && $this->key_prefix = $config['key_prefix'];

        if (isset($config['backup']) && in_array($config['backup'], $this->valid_drivers))
        {
            $this->_backup_driver = $config['backup'];
        }


        // 使用 is_supported() 判斷驅(qū)動器是否可用,不可用的話就降級到備用驅(qū)動器,如果還不可以用的話,就再次降級到 dummy,
        // dummy 這玩意就是空殼子,只是簡單實現(xiàn)抽象層的接口并返回true,起到了容錯的作用而已 !
        if ( ! $this->is_supported($this->_adapter))
        {
            if ( ! $this->is_supported($this->_backup_driver))
            {
                // Backup isn't supported either. Default to 'Dummy' driver.
                log_message('error', 'Cache adapter "'.$this->_adapter.'" and backup "'.$this->_backup_driver.'" are both unavailable. Cache is now using "Dummy" adapter.');
                $this->_adapter = 'dummy';
            }
            else
            {
                // Backup is supported. Set it to primary.
                log_message('debug', 'Cache adapter "'.$this->_adapter.'" is unavailable. Falling back to "'.$this->_backup_driver.'" backup adapter.');
                $this->_adapter = $this->_backup_driver;
            }
        }
    }

構(gòu)造方法中我們看到只是處理了傳入的參數(shù)解析到當前的屬性上面;然后判斷驅(qū)動器是否可用。

但是,有個問題讓人挺納悶的:

我們至今沒有看到驅(qū)動器的實例化,之前在 driver() 看到載入的驅(qū)動只是將 CI_Cache 這個驅(qū)動抽象類給實例化了,那么驅(qū)動器到底是在哪被被載入并實例化的?

在驅(qū)動器都還沒有實例化的情況下,竟然去根據(jù) is_supported() 判斷是否支持驅(qū)動器,這豈不是明顯的邏輯bug?

如果不是,那么會不會是在 is_supported() 中載入了驅(qū)動器并實例化了呢?

那只能進入 is_supported() 一探究竟了!

is_supported()

public function is_supported($driver)
{
        static $support = array();

        if ( ! isset($support[$driver]))
        {
            $support[$driver] = $this->{$driver}->is_supported();
        }

        return $support[$driver];
}

可以看到 is_supported() 就是在驅(qū)動抽象層對各個驅(qū)動 is_supported() 方法的包裝而已,我們依然沒有看到 驅(qū)動器的實例化!

我就不賣關(guān)子了,其實驅(qū)動器就是在 is_supported() 方法中被實例化的,你肯定會有疑問: "并沒有看見實例化驅(qū)動器的關(guān)鍵字 new 啊"? 是的,你確實沒看見,不見得驅(qū)動器沒被創(chuàng)建!

接下來我要說的我見到了截止目前分析 CI 框架源碼最精妙的一段代碼(哎!深邃的面向?qū)ο蟀?;
先說結(jié)論:驅(qū)動器是通過魔術(shù)方法給載入的 (想對 CI 說 FUCK U)?。?!

那魔術(shù)方法又在哪?

之前在 driver() 函數(shù)中看到有一行如下的代碼,魔術(shù)方法就出在載入的這個 Driver.php 中!

if ( ! class_exists('CI_Driver_Library', FALSE))
{
    // We aren't instantiating an object here, just making the base class available
    require BASEPATH.'libraries/Driver.php';
}

驅(qū)動器的載入和實例化就是在 Driver.php 實現(xiàn)的!,正如我們剛進入驅(qū)動抽象類 CI_Cache 看到的 CI_Driver_Library 類,這個 CI_Driver_Library 就位于Driver.php 中!


Driver.php 中驅(qū)動器的載入

Driver.php 中有兩個類,CI_Driver_Library 和 CI_Driver:

  • 前者被驅(qū)動抽象類 CI_Cache繼承用來載入驅(qū)動器;
  • 后者用來將 CI_Cache 這個抽象層的公共屬性和方法分配給每個子驅(qū)動器;

魔術(shù)方法是如何被觸發(fā)的?

我們知道,如果對象找不到某個屬性是就會觸發(fā)魔術(shù)方法,該魔術(shù)方法是在 is_supported() 中被觸發(fā)的!

image.png

注意看圖中的紅框部分:

由于 $driver 是驅(qū)動器,明顯在這里驅(qū)動器還沒被實例化,所以該驅(qū)動器是找不到的,那既然找不到,而 CI_Cache 又繼承了 CI_Driver_Library ,那必然會觸發(fā)其內(nèi)部的魔術(shù)方法 __get()!

接下來我們看下 __get() 的內(nèi)容

__get()

public function __get($child)
{
        // Try to load the driver
        return $this->load_driver($child);
}

注釋說的很清楚了,$child 就是驅(qū)動器的名字,通過 load_driver() 載入的!

load_driver()

    /*
     * load_driver 就簡單多了,就是從指定的 $path 下尋找到驅(qū)動然后實例化就行了
     */
    public function load_driver($child)
    {
        // 解析得到類名,反正 CI 自己的那套命名空間的的解析總是在 MY_,CI_ 這些命名空間倒來倒去
        $prefix = config_item('subclass_prefix');

        if ( ! isset($this->lib_name))
        {
            $this->lib_name = str_replace(array('CI_', $prefix), '', get_class($this));
        }

        //得到驅(qū)動器的名稱,注意沒前綴
        $child_name = $this->lib_name.'_'.$child;

        // valid_drivers 在 CI_Cache 中,是一個數(shù)組,定義了驅(qū)動器有哪些
        if ( ! in_array($child, $this->valid_drivers))
        {
            $msg = 'Invalid driver requested: '.$child_name;
            log_message('error', $msg);
            show_error($msg);
        }

        //很熟悉的套路了,得到 path,然后會從這些 path 去加載這個驅(qū)動器
        $CI = get_instance();
        $paths = $CI->load->get_package_paths(TRUE);

        // 加載擴展了的驅(qū)動器
        $class_name = $prefix.$child_name;
        $found = class_exists($class_name, FALSE);
        if ( ! $found)
        {
            // Check for subclass file
            foreach ($paths as $path)
            {
                // Does the file exist?
                $file = $path.'libraries/'.$this->lib_name.'/drivers/'.$prefix.$child_name.'.php';
                if (file_exists($file))
                {
                    // Yes - require base class from BASEPATH
                    $basepath = BASEPATH.'libraries/'.$this->lib_name.'/drivers/'.$child_name.'.php';
                    if ( ! file_exists($basepath))
                    {
                        $msg = 'Unable to load the requested class: CI_'.$child_name;
                        log_message('error', $msg);
                        show_error($msg);
                    }

                    // Include both sources and mark found
                    include_once($basepath);
                    include_once($file);
                    $found = TRUE;
                    break;
                }
            }
        }

        // 加載原生的驅(qū)動器
        if ( ! $found)
        {
            // Use standard class name
            $class_name = 'CI_'.$child_name;
            if ( ! class_exists($class_name, FALSE))
            {
                // Check package paths
                foreach ($paths as $path)
                {
                    // Does the file exist?
                    $file = $path.'libraries/'.$this->lib_name.'/drivers/'.$child_name.'.php';
                    if (file_exists($file))
                    {
                        // Include source
                        include_once($file);
                        break;
                    }
                }
            }
        }

        // 如果驅(qū)動抽象和驅(qū)動器都不存在就報錯
        if ( ! class_exists($class_name, FALSE))
        {
            if (class_exists($child_name, FALSE))
            {
                $class_name = $child_name;
            }
            else
            {
                $msg = 'Unable to load the requested driver: '.$class_name;
                log_message('error', $msg);
                show_error($msg);
            }
        }


        // 看到?jīng)],驅(qū)動器終于被實例化了,實例化后將該驅(qū)動作為屬性掛到驅(qū)動抽象層,
        // 然后就可以使用驅(qū)動器的 $this->{$driver}->is_supported() 去檢測該驅(qū)動是否可用了;
        $obj = new $class_name();
        $obj->decorate($this);
        $this->$child = $obj;
        return $this->$child;
}

這樣我們之前關(guān)于驅(qū)動器那幾個疑問就可以解答完畢了,可以看到驅(qū)動抽象層中調(diào)用未實例化的驅(qū)動器時觸發(fā)了魔術(shù)方法,進而實現(xiàn)了驅(qū)動器的載入,整個過程非常巧妙但又一開始讓人覺得非常復雜!

另外上面的 load_driver() 倒數(shù)第三行我們看到有個 decorate() 方法,這個方法做了一些畫龍點睛的事情,我們有必要看下!

decorate ()

decorate() 位于 Driver.php 的 CI_Driver 中, CI_Driver 類被各個驅(qū)動器給繼承了!

decorate() 看名字是裝飾的意思,它內(nèi)部其實就是對裝飾器模式的一種應用而已,而裝飾器模式的定義是:

為已有的功能動態(tài)的添加更多功能的一種方式;如果你需要擴展一個類的功能,并且不想增加子類的話,那么裝飾器模式就非常適合你!

public function decorate($parent)
{
        $this->_parent = $parent;

        $class_name = get_class($parent);

        // 利用反射將抽象類 CI_Cache 的公共屬性和方法給遍歷出來,并保存,
        // 至于保存起來要干嘛?我們待會說;
        // 關(guān)于什么是 PHP 中的反射見這里:http://php.net/manual/zh/book.reflection.php

        if ( ! isset(self::$_reflections[$class_name]))
        {
            $r = new ReflectionObject($parent);

            foreach ($r->getMethods() as $method)
            {
                if ($method->isPublic())
                {
                    $this->_methods[] = $method->getName();
                }
            }

            foreach ($r->getProperties() as $prop)
            {
                if ($prop->isPublic())
                {
                    $this->_properties[] = $prop->getName();
                }
            }

            self::$_reflections[$class_name] = array($this->_methods, $this->_properties);
        }
        else
        {
            list($this->_methods, $this->_properties) = self::$_reflections[$class_name];
        }
}

上面的代碼中我們看到,將抽象類的接口通過反射遍歷處理并保存,那么為什么要保存起來呢?

答案是:由于驅(qū)動抽象類和驅(qū)動器是非繼承關(guān)系,我們知道非繼承關(guān)系要保持接口對外的統(tǒng)一行為,如果驅(qū)動器中某個行為不存在,我們必須能夠為其動態(tài)添加該行為,所以看到 decorate() 將抽象層的行為給保存起來了!

那又是如何動態(tài)的為驅(qū)動器添加抽象的行為呢?

答案是:驅(qū)動器通過自身的魔術(shù)方法,當調(diào)用不存在的某個行為,就可以通過魔術(shù)方法調(diào)用保存$_reflections中抽象類的行為,所以你看,這不就是動態(tài)添加上了抽象層的行為么?活生生裝飾器模式的應運!

驅(qū)動器自身的魔術(shù)方法繼承自 CI_driver 類中,該類的這幾個魔術(shù)方法就是對 _reflections 中保存的抽象層行為的操作罷了!

public function __call($method, $args = array())
{
    if (in_array($method, $this->_methods))
    {
        return call_user_func_array(array($this->_parent, $method), $args);
    }

    throw new BadMethodCallException('No such method: '.$method.'()');
}

public function __get($var)
{
    if (in_array($var, $this->_properties))
    {
        return $this->_parent->$var;
    }
}

public function __set($var, $val)
{
    if (in_array($var, $this->_properties))
    {
        $this->_parent->$var = $val;
    }
}

驅(qū)動抽象和驅(qū)動器的載入實例化的源碼我們就分析到此了,代碼量雖然不多,可足夠復雜,可以說是解讀 CI 框架源碼以來最難讀的一部分源碼了!

接下來我們看下具體驅(qū)動器對于驅(qū)動抽象層接口實現(xiàn)的細節(jié),驅(qū)動器接口實現(xiàn)部分的源碼也沒多復雜,我們就只是看下 redis,file 這兩個驅(qū)動的接口實現(xiàn)!

File

整個 Cache_file 的實現(xiàn)很簡單,大家有興趣自己看下,幾行代碼一會就掃完了!

// 寫入緩存,注意:是以序列化的方式寫入的,并保存了緩存過期的時間
    public function save($id, $data, $ttl = 60, $raw = FALSE)
    {
        $contents = array(
            'time'      => time(),
            'ttl'       => $ttl,
            'data'      => $data
        );

        if (write_file($this->_cache_path.$id, serialize($contents)))
        {
            chmod($this->_cache_path.$id, 0640);
            return TRUE;
        }

        return FALSE;
    }

    // ------------------------------------------------------------------------


    public function get($id)
    {
        $data = $this->_get($id);
        return is_array($data) ? $data['data'] : FALSE;
    }

    // 讀取緩存,可以看到反序列化后還判斷了時間是否過期
    protected function _get($id)
    {
        if ( ! file_exists($this->_cache_path.$id))
        {
            return FALSE;
        }

        $data = unserialize(file_get_contents($this->_cache_path.$id));

        if ($data['ttl'] > 0 && time() > $data['time'] + $data['ttl'])
        {
            unlink($this->_cache_path.$id);
            return FALSE;
        }

        return $data;
    }



    // ------------------------------------------------------------------------
    
    //緩存刪除就是直接將文件干掉了
    public function delete($id)
    {
        return file_exists($this->_cache_path.$id) ? unlink($this->_cache_path.$id) : FALSE;
    }

Redis

CI 提供的對 redis 驅(qū)動非常輕量,只是做了一些簡單的包裝而已,缺失了 redis 的很多操作復雜數(shù)據(jù)結(jié)構(gòu)的接口,如果你對 redis 有一些復雜的使用,筆者不建議你使用 CI 自帶的 redis 驅(qū)動,你完全可以自己在封裝一個 redis 庫!

下面是筆者在 CI 中自己封裝了一個 redis 操作類,經(jīng)供參考

<?php
/**
 * Created by TCL
 * User: Administrator
 * Date: 2018/2/3
 * Time: 14:15
 */
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * Class Pedis
 */
class Pedis extends Redis
{
    /**
     * @var null
     */
    private static $init   = false;

    /**
     * @var null|Redis
     */
    public  $redis   = null;

    /**
     * @var null redis 配置文件
     */
    private $config  = null;

    /**
     * PRedis constructor.
     */
    public function __construct()
    {
        parent::__construct();

        if(self::$init == false)
        {
            $CI = & get_instance();

            $host   = $CI->config->item('host');
            $port   = $CI->config->item('port');
            $passwd = $CI->config->item('password');

            $this->connect($host, $port);
            $this->auth($passwd);
            self::$init = true;
        }
    }


    /**
     * 關(guān)閉 redis 連接
     */
    public function __destruct()
    {
        if (self::$init)
        {
            $this->close();
        }
    }
}

該類繼承 redis 后, 經(jīng)過這樣一層包裝,redis 中所有的接口你都可以通過該類使用!

至此整個緩存驅(qū)動的源碼就分析結(jié)束了!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容