Magento缓存与全局配置文件缓存

使用如下例子:
1 先关闭缓存
然后在任何一个控制器中添加一个方法加入如下代码:

1
2
3

$xml = Mage::getConfig()->getNode()->asXml();
file_put_contents('D:/config_file.xml', $xml);

在我这里,产生的文件大小为684K。这是一个非常让我吃惊的数字。如果每个请求都重复这个过程,如果100个同时请求,将吃掉68400K=68.4M内存,注意,这只是针对全局配置, 还没有包含布局系统等。

2 开启缓存
多次刷新刚才那个方法,发现产生的文件只有220K。

问题:为何会如此?比对先后两次产生的文件代码:
Magento全局配置文件结构

Magento全局配置文件结构

从缓存中获取的文件,admin adminhtml install stores crontab websites节点不见了。于是就产生了一个很大的困惑,如果开启了缓存,那么如何获取某个店铺的配置(因为缓存取回的配置没有店铺的设置)?

下面我们运行如下代码:

1
2
3

$xml = Mage::getConfig()->getNode('stores');
file_put_contents('D:/config_store.xml',$xml->asXml());

发现,它输出:

1
2
3
4
5

<store>
<default></defatul>
<admin></admin>
</store>

刚才消失的store节点这里获取出来了。

看起来,我们必须搞明白缓存对象干了什么事情。

首先进入App的run方法,它首先运行baseInit(),它里面有:

1
2
3

$cacheInitOptions = is_array($options) && array_key_exists('cache', $options) ? $options['cache'] : array(); //空
$this->_initCache($cacheInitOptions);

注意,这里的$cacheInitOptions是空的。然后调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

protected function _initCache(array $cacheInitOptions = array())
{
$this->_isCacheLocked = true;
$options = $this->_config->getNode('global/cache');
if ($options) {
$options = $options->asArray();
} else {
$options = array();
}
$options = array_merge($options, $cacheInitOptions);
$this->_cache = Mage::getModel('core/cache', $options);
$this->_isCacheLocked = false;
return $this;
}

可以看到,这里初始化了一个core/cache对象。另外,我们可以知道,它会从global/cache中获取配置信息,关于缓存配置的都是在这里设置。

接下看看Mage_Core_Model_Cache的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

public function __construct(array $options = array())
{
// ../var/cache
$this->_defaultBackendOptions['cache_dir'] = Mage::getBaseDir('cache');
/**
* Initialize id prefix
*/
// id_prefix
$this->_idPrefix = isset($options['id_prefix']) ? $options['id_prefix'] : '';
// id_prefix没有指定,但是设置了prefix,那么 id_prefix = prefix
if (!$this->_idPrefix && isset($options['prefix'])) {
$this->_idPrefix = $options['prefix'];
}
// 如果_idPrefix还为空,MD5 ../etc目录,然后取前3个字符,然后再加下划线作为_idPrefix
if (empty($this->_idPrefix)) {
$this->_idPrefix = substr(md5(Mage::getConfig()->getOptions()->getEtcDir()), 0, 3).'_';
}

$backend = $this->_getBackendOptions($options);
$frontend = $this->_getFrontendOptions($options);

$this->_frontend = Zend_Cache::factory('Varien_Cache_Core', $backend['type'], $frontend, $backend['options'],
true, true, true
);

if (isset($options['request_processors'])) {
$this->_requestProcessors = $options['request_processors'];
}

if (isset($options['disallow_save'])) {
$this->_disallowSave = $options['disallow_save'];
}
}

最终使用Zend_Cache生成缓存对象。$backend得到的是一个包含两个字段的数组,一个是type,表示存储类型,另一个是options,对应这个存储类型的选项。具体需要继续跟踪进入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

protected function _getBackendOptions(array $cacheOptions)
{
$enable2levels = false;
// 存储类型,默认为 File
$type = isset($cacheOptions['backend']) ? $cacheOptions['backend'] : $this->_defaultBackend;
// 检查是否提供了backend_options
if (isset($cacheOptions['backend_options']) && is_array($cacheOptions['backend_options'])) {
$options = $cacheOptions['backend_options'];
} else {
$options = array();
}
// 根据$type,获取$backendType,如果配置中没有给出backend_options,那么$options就是空的,但是根据不同的类型,还是可能产生其它$option
$backendType = false;
switch (strtolower($type)) {
case 'apc':
if (extension_loaded('apc') && ini_get('apc.enabled')) {
$enable2levels = true;
$backendType = 'Apc';
}
break;
case 'database':
$backendType = 'Varien_Cache_Backend_Database';
$options = $this->getDbAdapterOptions();
break;
default:
if ($type != $this->_defaultBackend) {
}
}

$backendOptions = array('type' => $backendType, 'options' => $options);
if ($enable2levels) {
$backendOptions = $this->_getTwoLevelsBackendOptions($backendOptions, $cacheOptions);
}
return $backendOptions;
}

$options的中的值根据不同存储类型会不同(还受到配置中是否给出backend_options配置的影响),可以推导出配置结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

<global>
<cache>
<backend></backend>
<backend_options>
<memcached>只有backend为memcached时</memcached>
</backend_options>

<auto_refresh_fast_cache><auto_refresh_fast_cache>
<slow_backend><slow_backend>
<slow_backend_options><slow_backend_options>
<slow_backend_store_data><slow_backend_store_data>
<cache_db_complete_path>只有backend为sqlite时</ cache_db_complete_path/>
<slow_backend_store_data>只有backend为database时</slow_backend_store_data>

<frontend_options>
<caching>不设置默认为true</caching>
<lifetime>不设置默认是7200</lifetime>
<automatic_cleaning_factor>不设置默认为0</automatic_cleaning_factor>
</frontend_options>
</cache>
</global>

如果类型是memcached时,backend_options应该需要提供memcached节点。

注意_getBackendOptions函数返回$backendOptions前,还经过了_getTwoLevelsBackendOptions方法的处理(除了类型是File和Database时),所以最终返回的数据:

1
2
3
4
5
6
7
8
9
10
11
12

$options['fast_backend'] //存储类型,根据backend节点转换而来
$options['fast_backend_options'] //只有backend节点为database和memcached时有内容
$options['fast_backend_custom_naming'] = true;
$options['fast_backend_autoload'] = true;
$options['slow_backend_custom_naming'] = true;
$options['slow_backend_autoload'] = true;

$options['auto_refresh_fast_cache'] //来自auto_refresh_fast_cache节点,否则为false
$options['slow_backend'] //来自slow_backend节点,否则为File
$options['slow_backend_options'] // 来自slow_backend_options节点,否则为默认
$options['slow_backend_options']['store_data']//来自slow_backend_store_data,只有backend是database时,不过看起来它不可进入

回到缓存对象的构造函数,接下来执行_getFrontendOptions函数,它去寻找frontend_options节点,设置是否缓存 和 缓存时间,参考以上给出的配置文件。

接下执行:

1
2
3
4

$this->_frontend = Zend_Cache::factory('Varien_Cache_Core', $backend['type'], $frontend, $backend['options'],
true, true, true
);

实际利用Zend_Cache生成了一个缓存对象(Varien_Cache_Core继承自Zend_Cache_Core),它保存到缓存对象的_frontend中(**)。

通过App的_initCache,它的_cache字段都保持了一份Mage_Core_Model_Cache对象的引用(注意它内部的__frontend,它应该才是核心)。

现在把目光转移回到App的run方法中,看如下代码:

1
2
3
4

if ($this->_cache->processRequest()) {
$this->getResponse()->sendResponse();
} else {}

这里调用缓存对象的processRequest,如果顺利,可以直接响应。那么去看看processRequest方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public function processRequest()
{
if (empty($this->_requestProcessors)) {
return false;
}

$content = false;
foreach ($this->_requestProcessors as $processor) {
$processor = $this->_getProcessor($processor);
if ($processor) {
$content = $processor->extractContent($content);
}
}

if ($content) {
Mage::app()->getResponse()->appendBody($content);
return true;
}
return false;
}

马上就有疑问,_requestProcessors如何被设置的,好吧,在构造函数中,刚才少说了如下代码:

1
2
3
4
5

// Mage_Core_Model_Cache
if (isset($options['request_processors'])) {
$this->_requestProcessors = $options['request_processors'];
}

request_processors是在配置文件的cache中指定的:

1
2
3
4
5
6

<global>
<cache>
<request_processors></request_processors>
</cache>
</global>

如果指定了,那么就使用它处理响应。这个处理器至少有extractContent方法,看起来应该是和全页缓存相关。我试图去搜索extractContent,没有找到这个方法,结论是,Magento CE还没有提供全页缓存,这个也是这种插件热卖的原因了。

如果进入System->Configuration->ADVANCED->System,你将看到:
Magento后台设置全页缓存

Magento后台设置全页缓存
这个配置和我这里说的全页缓存不是一个概念。这里的zend_page_cache是Zend Server提供的一个扩展,具体怎么搞就没有研究了。
Zend_Page_Cahce配置

Zend_Page_Cahce配置
这个探讨先到这里。我们继续回到App的_initModules()方法中:

1
2
3
4
5
6
7
8
9
10
11

protected function _initModules()
{
if (!$this->_config->loadModulesCache()) {
$this->_config->loadModules();

$this->_config->loadDb();
$this->_config->saveCache();
}
return $this;
}

这里的saveCache()方法是首次开始缓存。既然如此,那么继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public function saveCache($tags=array())
{
if (!Mage::app()->useCache('config')) {
return $this;
}
//为缓存打标签
if (!in_array(self::CACHE_TAG, $tags)) {
$tags[] = self::CACHE_TAG;
}
$cacheLockId = $this->_getCacheLockId();
//这是上锁保护 如果缓存正在进行,那么就被取消
if ($this->_loadCache($cacheLockId)) {
return $this;
}
// … 省略
}

这里首先使用App的useCache方法检查是否启用了缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

//Mage_Core_Model_App
public function useCache($type=null)
{
return $this->_cache->canUse($type);
}
//Mage_Core_Model_Cache
public function canUse($typeCode)
{
// _allowedCacheOptions为空
if (is_null($this->_allowedCacheOptions)) {
$this->_initOptions();
}

if (empty($typeCode)) {
return $this->_allowedCacheOptions;
} else {
if (isset($this->_allowedCacheOptions[$typeCode])) {
return (bool)$this->_allowedCacheOptions[$typeCode];
} else {
return false;
}
}
}

protected function _initOptions()
{
// OPTIONS_CACHE_ID->core_cache_options 这里首先从缓存中获取这个值
$options = $this->load(self::OPTIONS_CACHE_ID);
if ($options === false) { //没有获取值
//从数据库获取值
$options = $this->_getResource()->getAllOptions();
if (is_array($options)) {
$this->_allowedCacheOptions = $options;
//把值缓存起来
$this->save(serialize($this->_allowedCacheOptions), self::OPTIONS_CACHE_ID);
} else {
$this->_allowedCacheOptions = array();
}
} else {
//取到了值
$this->_allowedCacheOptions = unserialize($options);
}

if (Mage::getConfig()->getOptions()->getData('global_ban_use_cache')) {
foreach ($this->_allowedCacheOptions as $key => $val) {
$this->_allowedCacheOptions[$key] = false;
}
}

return $this;
}

这里先开个小差,哪些东西可以设置缓存,后台可以指定:
Magento缓存管理

Magento缓存管理

这些设置保存到表core_cache_option中:
Magento缓存配置表

Magento缓存配置表
实际canUse就是返回对应的value值,1表示使用缓存,0表示不使用。另外,还有core_cache和core_cache_tag这两个表和core_cache_option没有任何干系,它们是使用数据库缓存时保存缓存数据的地方。

回到saveCache方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

//Mage_Core_Model_Config
public function saveCache($tags=array())
{
if (!Mage::app()->useCache('config')) {
return $this;
}
//为缓存打标签 保存的时候可以打标签
if (!in_array(self::CACHE_TAG, $tags)) {
$tags[] = self::CACHE_TAG;
}
// Config对象在构造函数中$this->setCacheId('config_global');设置了ID
// 这里获取config_global.lock,如果能load进来,直接就返回了,说明在上锁
$cacheLockId = $this->_getCacheLockId();
if ($this->_loadCache($cacheLockId)) {
return $this;
}
/*
$_cacheSections = array(
'admin' => 0,
'adminhtml' => 0,
'crontab' => 0,
'install' => 0,
'stores' => 1,
'websites' => 0
);
*/
if (!empty($this->_cacheSections)) {
$xml = clone $this->_xml;
//根据_cacheSections数组把全局配置文件拆分
foreach ($this->_cacheSections as $sectionName => $level) {
$this->_saveSectionCache($this->getCacheId(), $sectionName, $xml, $level, $tags);
unset($xml->$sectionName);
}
// config_global 是被剥离后剩余部分
$this->_cachePartsForSave[$this->getCacheId()] = $xml->asNiceXml('', false);
} else {
return parent::saveCache($tags);
}

$this->_saveCache(time(), $cacheLockId, array(), 60); //上锁
//移除打了CONFIG标签的cache(全部配置相关的缓存,全部打了这个标志,默认)
$this->removeCache();
foreach ($this->_cachePartsForSave as $cacheId => $cacheData) {
$this->_saveCache($cacheData, $cacheId, $tags, $this->getCacheLifetime());
/*
protected function _saveCache($data, $id, $tags=array(), $lifetime=false)
{
return Mage::app()->saveCache($data, $id, $tags, $lifetime);
}
*/
}
unset($this->_cachePartsForSave);
$this->_removeCache($cacheLockId); //解锁
return $this;
}

protected function _saveSectionCache($idPrefix, $sectionName, $source, $recursionLevel=0, $tags=array())
{
if ($source && $source->$sectionName) {
// config_global_admin config_global_websites
$cacheId = $idPrefix . '_' . $sectionName;
if ($recursionLevel > 0) {
foreach ($source->$sectionName->children() as $subSectionName => $node) {
$this->_saveSectionCache(
$cacheId, $subSectionName, $source->$sectionName, $recursionLevel-1, $tags
);
}
}
//部分保存的XML
$this->_cachePartsForSave[$cacheId] = $source->$sectionName->asNiceXml('', false);
}
return $this;
}

这部分代码总体上实现了把全局对象拆分成几段缓存:

config_global
config_global_admin
config_global_adminhtml
config_global_install
config_global_websites
config_global_stores_default
config_global_stores_admin
config_global_stores_german
config_global_stores_french
config_global是被拆了admin等之后剩余的XML。这些缓存全部打上了CONFIG这个Tag,Config中的removeCache方法实际就是移除所有打上了这个Tag的缓存。

生成缓存就这样够一段了。下面如何加载缓存。还是从App的run方法进入,它之中运行的baseInit主要是初始化环境(_initEnvironment),初始化基础配置(app/etc/config.xml和local.xml,由_initBaseConfig调用Config的loadBase完成,说明这两个文件的内容尽管已经缓存,但是每次都会被读取)和生成缓存对象,接下来的_initModules方法:

1
2
3
4
5
6
7

protected function _initModules()
{
if (!$this->_config->loadModulesCache()) {
}
return $this;
}

实际调用Config的loadModulesCache方法加载缓存,如果能加载就使用加载的缓存文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

//Mage_Core_Model_Config
public function loadModulesCache()
{
if (Mage::isInstalled(array('etc_dir' => $this->getOptions()->getEtcDir()))) {
if ($this->_canUseCacheForInit()) {
$loaded = $this->loadCache();

if ($loaded) {
$this->_useCache = true;
return true;
}
}
}
return false;
}

这里重点是loadCache方法,这个负责加载缓存,如果成功加载使用_useCache标识在使用缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// lib/Varien/Simplexml/Config
public function loadCache()
{
if (!$this->validateCacheChecksum()) {
return false;
}

$xmlString = $this->_loadCache($this->getCacheId());
$xml = simplexml_load_string($xmlString, $this->_elementClass);
if ($xml) {
$this->_xml = $xml;
$this->setCacheSaved(true);
return true;
}

return false;
}

getCacheId()获取config_global,它会把这个缓存加载进来。所以从缓存拿回来的_xml只是一部分,这才解释了本文刚开始遇到的疑问。可是新疑问又来了,既然是部分配置,那么如何获取店铺的配置呢?

首先是获取配置的用法:

1
2
3
4

$config->getNode('admin');
$config->getNode('stores');
$config->getNode('stores/default');

进入getNode()方法就可以看到,它根据第一个字段,相应的从缓存中取出缓存的内容。注意,其它获取配置的包装方法,都是间接使用getNode()方法,比如Mage::getStoreConfig()方法,实际调用Mage_Core_Model_Store的getConfig方法,而这个方法内部就是调用getNode:

1
2
3
4
5
6

public function getConfig($path)
{
$fullPath = 'stores/' . $this->getCode() . '/' . $path;
$data = $config->getNode($fullPath);
}

最后总结一下Magento中使用cache的方法:

1
2
3
4
5
6
7
8
9

//缓存一块数据
saveCache($data, $id, $tags=array(), $lifeTime=false)
//根据ID加载缓存
loadCache($id)
//根据ID删除缓存
removeCache($id)
//根据TAGS清楚缓存
cleanCache($tags=array())

来一段测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

$data = "12345678901234567890000000";
$id = "Test_Cache";
$tags = array('xTest','xCache');

$fromCache = Mage::app()->loadCache($id);
if($fromCache){
echo "From cache--->
";
echo $fromCache;
Mage::app()->removeCache($id);
}else{
echo "No cache --->
";
Mage::app()->saveCache($data, $id, $tags);
}

Magento缓存文件

Magento缓存文件

Magento缓存文件

Magento缓存文件内容

这种缓存的功能是ZF提供的。对于全局配置文件,如果不缓存,每次都读取合并很多文件,这个过程将产生大量的IO,如果缓存了,将可以减少大部分的IO操作和减少计算资源,但是还是要把缓存读入内存,如果能够把这些缓存放入共享内存中,理论上应该可以提升性能(减少了从磁盘调人内存这个IO操作,所以把缓存放入磁盘和放入共享内存,性能提升不明显的原因就在这里),如缓存已经在内存中,不用每次都调用文件写入内存,但是获取缓存转换成PHP对象仍然占有比较多内存,所以Magento是很非常耗内存,并发一多,就会很明显。

坚持原创技术分享,您的支持将鼓励我继续创作!