Laravel 源码分析系列:Cache

缓存源码分析

Laravel的cache使用起来特别方便,支持多种缓存驱动,包括apc,database, file, memcache,redis,xcache,wincache,Laravel是如何做到同时支持这么多缓存驱动的呢,又是如何做到可以多种驱动快速切换呢,如何让所有的缓存驱动都使用统一的调用方法呢,我们今天就来分析一下Laravel的cache部份源码一探究竟吧。

基本流程分析

流程图

具体分析

从get开始

我们一般获取一个cache的代码类似Cache::get('cacheKey'),熟悉Laravel的facade外观模式的话知道这里其实就是在使用Cache这个facade。通过查看config/app.php中的aliases,可以看到实际使用的是:

'Cache'     => Illuminate\Support\Facades\Cache::class,

cache外观 - Cache Facade

Cache的外观模式,代码如下:

<?php

namespace Illuminate\Support\Facades;

class Cache extends Facade
{
    /**
     * 获取注册注件的名称
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'cache';
    }
}

这个类比较简单,就只有一个方法,方法返回了一个cache字符串,这里面涉及到facade的概念,我们在本节暂时不多说,后面会单独介绍,在这里只是告诉大家这里最后会得到一个在Laravel服务容器中的cache服务,所以我们需要去找到这个服务是在哪注册。

config/app.phpproviders我们可以发现有一个Illuminate\Cache\CacheServiceProvider::class,这就是我们的cache服务提供者。

cache服务提供者 - CacheServiceProvider

文件位置:src/vendor/laravel/framework/src/Illuminate/Cache/CacheServiceProvider.php

代码如下:

<?php

namespace Illuminate\Cache;

use Illuminate\Support\ServiceProvider;
use Illuminate\Cache\Console\ClearCommand;
use Illuminate\Cache\Console\CacheTableCommand;

class CacheServiceProvider extends ServiceProvider
{
    /**
     * 服务提供者是否延迟加载
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * 注册服务提供者
     *
     * @return void
     */
    public function register()
    {
        // 在服务容器中设置一个名为cache的CacheManager单例
        $this->app->singleton('cache', function ($app) {
            return new CacheManager($app);
        });

        // 在服务容器中设置一个名为cache.store的单例,比如memcache database
        $this->app->singleton('cache.store', function ($app) {
            return $app['cache']->driver();
        });

        // 在服务容中设置一个名为memcached.connector的MemcachedConnector单例,
        $this->app->singleton('memcached.connector', function () {
            return new MemcachedConnector;
        });

        // 注册缓存相关的控制台命令
        $this->registerCommands();
    }

    /**
     * 注册缓存相关的控制台命令。
     *
     * @return void
     */
    public function registerCommands()
    {
        // 清除缓存
        $this->app->singleton('command.cache.clear', function ($app) {
            return new ClearCommand($app['cache']);
        });

        // 创建database缓存驱动的存储表
        $this->app->singleton('command.cache.table', function ($app) {
            return new CacheTableCommand($app['files'], $app['composer']);
        });

        // 注册这两个命令
        $this->commands('command.cache.clear', 'command.cache.table');
    }

    /**
     * 提供的服务提供者。
     *
     * @return array
     */
    public function provides()
    {
        return [
            'cache', 'cache.store', 'memcached.connector', 'command.cache.clear', 'command.cache.table',
        ];
    }
}

这样我们最后通过Cache::get()访问的实际是CacheManager类,
也就是类似

<?php
$cache = new CacheManager($app);
$cache->get('cacheKey');

Cache Manager

我们看上面的代码在调用$cache->get(),那我们去找找
CacheManager代码看看
文件位置:src/vendor/laravel/framework/src/Illuminate/Cache/CacheManager.php
在代码中我们发现有定义一个get方法:

<?php
/**
* 从本地的stores变量中获取缓存仓库,如果没有根据名称创建仓库
*
* @param  string  $name
* @return \Illuminate\Contracts\Cache\Repository
*/
protected function get($name)
{
    return isset($this->stores[$name]) ? $this->stores[$name] : $this->resolve($name);
}

但这个方法的是protected的,在外部无法访问。所以这时候会调用魔术方法__call

<?php
/**
* 动态调用默认驱动程序实例。
*
* @param  string  $method
* @param  array   $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
    return call_user_func_array([$this->store(), $method], $parameters);
}

首先调用$this->store(), 代码如下:

<?php
/**
* 根据名称获取缓存存储实例
*
* @param  string|null  $name
* @return mixed
*/
public function store($name = null)
{
    // 获取默认驱动名称
    $name = $name ?: $this->getDefaultDriver();

    // 根据名称获取缓存仓库
    return $this->stores[$name] = $this->get($name);
}

看一下这里的$this->getDefaultDriver(),代码如下:

<?php
/**
* 获取默认的缓存驱动名称
*
* @return string
*/
public function getDefaultDriver()
{
    // 实际获取的是config/cache.php中的数组,key为default,没有修改过的话默认是 'default' => env('CACHE_DRIVER', 'file'),
    return $this->app['config']['cache.default'];
}

根据上面的代码可以得到最后我们的$name默认的为.env中定义的CACHE_DRIVER,没有修改过的时候默认是CACHE_DRIVER=file,我们这里修改为redis
所以最后$name = 'redis'

接着跟踪$this->get($name),代码如下:

<?php
/**
* 从本地的stores变量中获取缓存仓库,如果没有根据名称创建仓库
*
* @param  string  $name
* @return \Illuminate\Contracts\Cache\Repository
*/
protected function get($name)
{
    return isset($this->stores[$name]) ? $this->stores[$name] : $this->resolve($name);
}

接着跟踪$this->resolve($name),代码如下:

<?php
/**
* 使用给定的名称创建缓存驱动
*
* @param  string  $name
* @return \Illuminate\Contracts\Cache\Repository
*
* @throws \InvalidArgumentException
*/
protected function resolve($name)
{
    // 根据名称获取配置信息
    $config = $this->getConfig($name);

    // 没有配置信息则抛异常
    if (is_null($config)) {
        throw new InvalidArgumentException("Cache store [{$name}] is not defined.");
    }

    // 判断是不是自定义的驱动类型,如果是则调用
    if (isset($this->customCreators[$config['driver']])) {
        return $this->callCustomCreator($config);
    } else {
        // 根据配置信息组装创建驱动的方法名
        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

        // 判断是否存在方法,存在则调用具体的方法创建驱动并返回
        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($config);
        } else {
            throw new InvalidArgumentException("Driver [{$config['driver']}] not supported.");
        }
    }
}

以redis为例,最后会调用$this->createRedisDriver($config), 代码如下:

<?php
/**
* 创建一个redis缓存驱动实例
*
* @param  array  $config
* @return \Illuminate\Cache\RedisStore
*/
protected function createRedisDriver(array $config)
{
    // 获取redis实例
    $redis = $this->app['redis'];

    // 获取redis连接信息
    $connection = Arr::get($config, 'connection', 'default');

    // 通过redis存储创建缓存库
    return $this->repository(new RedisStore($redis, $this->getPrefix($config), $connection));
}

可以看到最后将实例化的new RedisStore作为参数,传给$this->repository,继续追踪代码如下:

<?php
/**
* 通过给定的缓存存储实现创建一个缓存库
*
* @param  \Illuminate\Contracts\Cache\Store  $store
* @return \Illuminate\Cache\Repository
*/
public function repository(Store $store)
{
    // 创建缓存库
    $repository = new Repository($store);

    // 如果没有设置过事件调度器则设置事件调度器实例
    if ($this->app->bound('Illuminate\Contracts\Events\Dispatcher')) {
        $repository->setEventDispatcher(
            $this->app['Illuminate\Contracts\Events\Dispatcher']
        );
    }

    return $repository;
}

可以看到,我们最后得到了一个Repository的实例。

我们总结一下中间的过程,伪代码如下:

$cache->get();
    ↓
CacheManager::__call()
    ↓
$this->store()
    ↓
$this->get($name); <- $name = $this->getDefaultDriver();
    ↓
$this->resolve($name);
    ↓
$this->createRedisDriver($config); <- $config = $this->getConfig($name);
    ↓
$this->repository(new RedisStore($redis, $this->getPrefix($config), $connection));
    ↓
return $repository = new Repository($store);

也就是说最后我们实际是$repository->get('cacheKey');,接下来我们就需要分析Repository类了

Cache Repository

Cache Store 存储分析

Database

File

Memcache

Redis

Apc

Xcache

WinCache

总结

本帖已被设为精华帖!
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 12
Summer

对于阅读 Laravel 核心源码来说,很不错的突破口 :+1:

7年前 评论

源码分析很好,希望后续有更多的分析

7年前 评论

不分析 container ?

7年前 评论

最近在看依赖注入,请问cache是在哪一步骤完成依赖注入的。

7年前 评论

@haoyuqi vendor/laravel/framework/src/Illuminate/Cache/CacheServiceProvider.php

 $this->app->singleton('cache', function ($app) {
         return new CacheManager($app);
  });
7年前 评论

接下来我们就需要分析Repository类了

where ?

谁发个链接

6年前 评论
sushengbuhuo

一年了,楼主还没更新

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!