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

总结

本帖已被设为精华帖!
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 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年前 评论

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