分类目录归档:Magento

Magento 1.x Nginx配置备忘

user              nginx;
worker_processes  1;
error_log         /var/log/nginx/error.log;
pid               /var/run/nginx.pid;
  
events {
    worker_connections  1024;
}
  
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
  
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request "'
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
  
    sendfile        on;
    autoindex off;
    map $scheme $fastcgi_https { ## Detect when HTTPS is used
        default off;
        https on;
    }
  
    keepalive_timeout  10;
  
    gzip  on;
    gzip_comp_level 2;
    gzip_proxied any;
    gzip_types      text/plain text/html text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;
  
    # Load config files from the /etc/nginx/conf.d directory
    include /etc/nginx/conf.d/*.conf;
  
} 

server {
    listen 80;
    server_name xxx.com;
    rewrite / $scheme://www.$host$request_uri permanent;
}

server {
    listen 80 default;
    ## SSL directives might go here
    server_name www.xxx.com *.xxx.com;
    root /var/www/www.xxx.com;

    location / {
        index index.html index.php;
        try_files $uri $uri/ @handler;
        expires 30d;
    }

    location ^~ /app/                { deny all; }
    location ^~ /includes/           { deny all; }
    location ^~ /lib/                { deny all; }
    location ^~ /media/downloadable/ { deny all; }
    location ^~ /pkginfo/            { deny all; }
    location ^~ /report/config.xml   { deny all; }
    location ^~ /var/                { deny all; }

    location /var/export/ {
        auth_basic           "Restricted";
        auth_basic_user_file htpasswd;
        autoindex            on;
    }

    location  /. {
        return 404;
    }

    location @handler {
        rewrite / /index.php;
    }

    location ~ .php/ {
        rewrite ^(.*.php)/ $1 last;
    }

    location ~ .php$ {
        if (!-e $request_filename) { rewrite / /index.php last; }

        expires        off;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_param  HTTPS $fastcgi_https;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        fastcgi_param  MAGE_RUN_CODE default;
        fastcgi_param  MAGE_RUN_TYPE store;
        include        fastcgi_params;
    }
}

Magento系统加速 – MySQL读写分离

Magento使用EAV模型,所以对数据库来说,读操作往往比较大,可以使用读写分离技术有效解决这个问题。所谓读写分离,简单来说就是所有写操作,在A服务器上完成,所有写操作到B服务器上完成,A和B服务器实现主从复制。

A和B数据库服务器实现主从复制,MySQL提供了内置支持。可以把读压力水平分解(多个从库)。MySQL主从复制可以参考:http://www.vfeelit.com/353.html

Magento程序本身对读写分离提供了内置支持,可以为每个模块都指定读写适配器,如果没有指定就使用默认的读写适配器,而默认的读写适配器又是使用安装时的适配器。

比如模块名叫eav,那么可以在配置文件中指定eav_write和eav_read适配器,如果没有指定就使用系统默认的default_write和default_read,而default_write和default_read通过use节点指定它实际使用default_setup配置的适配器。

# app/etc/local.xml
    <global>
        <resources>
            <db>
                <table_prefix><![CDATA[]]></table_prefix>
            </db>
            <default_setup>
                <connection>
                    <host><![CDATA[192.168.1.10]]></host>
                    <username><![CDATA[magento]]></username>
                    <password><![CDATA[root]]></password>
                    <dbname><![CDATA[magento]]></dbname>
                    <initStatements><![CDATA[SET NAMES utf8]]></initStatements>
                    <model><![CDATA[mysql4]]></model>
                    <type><![CDATA[pdo_mysql]]></type>
                    <pdoType><![CDATA[]]></pdoType>
                    <active>1</active>
                </connection>
            </default_setup>
        </resources>
    </global>

默认的读写适配器是放置在app/etc/config.xml中的:

            <default_write>
                <connection>
                    <use>default_setup</use>
                </connection>
            </default_write>
            <default_read>
                <connection>
                    <use>default_setup</use>
                </connection>
            </default_read>

默认它使用use节点指定使用default_setup适配器,所以我们修改这两个配置就可以实现读写分离:

            <default_write>
                <connection>
                    <host><![CDATA[192.168.1.10]]></host>
                    <username><![CDATA[magento]]></username>
                    <password><![CDATA[root]]></password>
                    <dbname><![CDATA[magento]]></dbname>
                    <initStatements><![CDATA[SET NAMES utf8]]></initStatements>
                    <model><![CDATA[mysql4]]></model>
                    <type><![CDATA[pdo_mysql]]></type>
                    <pdoType><![CDATA[]]></pdoType>
                    <active>1</active>
                </connection>
            </default_write>
            <default_read>
                <connection>
                    <host><![CDATA[192.168.1.20]]></host>
                    <username><![CDATA[magento]]></username>
                    <password><![CDATA[root]]></password>
                    <dbname><![CDATA[magento]]></dbname>
                    <initStatements><![CDATA[SET NAMES utf8]]></initStatements>
                    <model><![CDATA[mysql4]]></model>
                    <type><![CDATA[pdo_mysql]]></type>
                    <pdoType><![CDATA[]]></pdoType>
                    <active>1</active>
                </connection>
            </default_read>

读操作全部到达192.168.1.20服务器,写操作全部在192.168.1.10进行。注意,不要在local.xml中修改这个配置。虽然local.xml的配置最优先,但是默认配置有use节点,只要有use节点,就使用use节点的适配器,所以需要在config.xml中直接修改(不要出现use节点)。

每个模块都可以指定自己的读写适配器,如果没有指定就使用默认的配置。这个为读压力的水平分解提供了内置支持。比如某个模块的读操作非常频繁,可以读定向到单独到一个服务器中。通过指定读写适配器,还可以为模块指定使用另一个库等等。

注意,读写分离时,由于采用了MySQL的主从复制,复制的数据库库决不能进行插入更新这种操作(否则可能导致从库无法同步),实际上,程序可能无法百分百保证初始化一个读适配器后不进行更新操作,所以,为了防止这个情况,只给读适配器这个用户Select权限,就能从数据库级别杜绝这个情况发生。

Magento读写分离权限控制

永久链接:http://blog.ifeeline.com/745.html
原创文章,转载务必保留出处。

Magento缓存配置与Zend_Cache_Backend_TwoLevels

如果有一个快的存储介质,一个慢的存储介质,可以同时保存缓存到这两个介质中,当快的存储介质使用率到达一定值(比如80%)的时候,把缓存保存到慢速介质,然后删除快速介质中的同版本缓存以释放快速存储介质空间,当获取缓存时首先到快的存储介质中获取,没有命中就到慢的介质中获取,当删除时快的慢的都一起删除。这种方式,当快的介质用满时,可以保存到慢速介质中,这样就不至于接下来的需要缓存的内容缓存失败获取淘汰最旧的缓存。

Zend Framework提供了这个实现(Zend_Cache_Backend_TwoLevels),Magento中当后端存储类型是memcached 和 apc 和 xcache 和 eaccelerator时,默认把后端类型调整为TwoLevels。

首先观察构造函数:

// Zend_Cache_Backend_TwoLevels
    public function __construct(array $options = array())
    {
        parent::__construct($options);

        if ($this->_options['slow_backend'] === null) {
        } else {
            $this->_slowBackend = Zend_Cache::_makeBackend(
                $this->_options['slow_backend'],
                $this->_options['slow_backend_options'],
                $this->_options['slow_backend_custom_naming'],
                $this->_options['slow_backend_autoload']
            );
        }

        if ($this->_options['fast_backend'] === null) {
        } else {
            $this->_fastBackend = Zend_Cache::_makeBackend(
                $this->_options['fast_backend'],
                $this->_options['fast_backend_options'],
                $this->_options['fast_backend_custom_naming'],
                $this->_options['fast_backend_autoload']
            );
        }

        $this->_slowBackend->setDirectives($this->_directives);
        $this->_fastBackend->setDirectives($this->_directives);
    }

这个构造函数删除了点内容。后端类都继承自Zend_Cache_Backend,它的构造函数就是把传递进来的$options循环放入到_options数组中。然后利用这些参数进行快慢两个后端进行初始化。从类提供的配置默认值:

    protected $_options = array(
        'slow_backend' => 'File',
        'fast_backend' => 'Apc',
        'slow_backend_options' => array(),
        'fast_backend_options' => array(),
        'slow_backend_custom_naming' => false,
        'fast_backend_custom_naming' => false,
        'slow_backend_autoload' => false,
        'fast_backend_autoload' => false,

        'stats_update_factor' => 10,
        'auto_refresh_fast_cache' => true
    );

前面的4对应该比较容易理解。auto_refresh_fast_cache发生在load函数中,当从快速介质中取回一个缓存时,实际上这个时候它的生存时间是需要修改的,这个设置就是用来刷新快速缓存(如果是做长久缓存的,就没有必要)。

stats_update_factor默认是10,10 表示10分之1的几率,这个情况是当已经获取了快速存储的利用率的情况下,比如50%,那么接下来每次获取利用率时,有10分之1的几率重新去获取,剩下的10分之9几率直接使用已经设置的值(这里的50%)

这个类的其它的方法其实是快慢后端二次封装,所以使用方法类似。

Magento中的缓存完美应用ZF提供的实现。Magento中使用了Mage_Core_Model_Cache对缓存提供了封装,这个对象首次初始化是在App的_initCache方法中:

    $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);

这里的$options直接取global/cache的配置传入构造函数。

//Mage_Core_Model_Cache,构造函数中_getTwoLevelsBackendOptions将被调用
    protected function _getTwoLevelsBackendOptions($fastOptions, $cacheOptions)
    {
        $options = array();
        $options['fast_backend']                = $fastOptions['type'];
        $options['fast_backend_options']        = $fastOptions['options'];
        $options['fast_backend_custom_naming']  = true;
        $options['fast_backend_autoload']       = true;
        $options['slow_backend_custom_naming']  = true;
        $options['slow_backend_autoload']       = true;

        if (isset($cacheOptions['auto_refresh_fast_cache'])) {
            $options['auto_refresh_fast_cache'] = (bool)$cacheOptions['auto_refresh_fast_cache'];
        } else {
            $options['auto_refresh_fast_cache'] = false;
        }
        if (isset($cacheOptions['slow_backend'])) {
            $options['slow_backend'] = $cacheOptions['slow_backend'];
        } else {
            $options['slow_backend'] = $this->_defaultBackend;
        }
        if (isset($cacheOptions['slow_backend_options'])) {
            $options['slow_backend_options'] = $cacheOptions['slow_backend_options'];
        } else {
            $options['slow_backend_options'] = $this->_defaultBackendOptions;
        }
        if ($options['slow_backend'] == 'database') {
            $options['slow_backend'] = 'Varien_Cache_Backend_Database';
            $options['slow_backend_options'] = $this->getDbAdapterOptions();
            if (isset($cacheOptions['slow_backend_store_data'])) {
                $options['slow_backend_options']['store_data'] = (bool)$cacheOptions['slow_backend_store_data'];
            } else {
                $options['slow_backend_options']['store_data'] = false;
            }
        }

        $backend = array(
            'type'      => 'TwoLevels',
            'options'   => $options
        );
        return $backend;
    }
变量
//$fastOptions  = $backendOptions
$fastOptions[‘type’] = $backendType
$fastOptions[‘options’] = $options  //->  global/cache/backend_options

$cacheOptions ->  global/cache/

当然$fastOptions[‘type’]为memcached,检查PHP扩展存在memcached时,$fastOptions[‘options’]被global/cache/memcached配置覆盖,并且此时后端的类型是Libmemcached,PHP扩展不存在memcached而memcache存储时,$fastOptions[‘options’]被global/cache/memcached配置覆盖,并且此时后端的类型是Memcached。

这个说明,后端的类型是Memcached时,backend_options节点无效(对files,sqlite有效),当后端的类型是databases时(),backend_options节点也是无效的。

由此我们可以得出如下配置结构:

<global>
<cache>
	<id_prefix>vfeelit</id_prefix>

	<backend>file|database|sqlite|memcached|apc|xcache|eaccelerator</backend>
        <memcached>memcached类型时</memcached>

	<backend_options>
		<cache_db_complete_path>类型是sqlite时</cache_db_complete_path>
        </backend_options>

        <auto_refresh_fast_cache>默认为false</auto_refresh_fast_cache>
        <slow_backend>默认为file</slow_backend>
        <slow_backend_options></slow_backend_options>
        <slow_backend_store_data>是database时</slow_backend_store_data>
</cache>
</global>

慢速存储一般是file,所以backend_options后面的参数可以不用指定。如果不希望使用默认设置,比如保存在不同目录等,如下是所有可用配置设置(单独使用file作为后端时,很多参数无法自定义):

<global>
	<cache>
        <slow_backend>默认为file<slow_backend>
        <slow_backend_options>
	    <cache_dir>null</cache_dir>
            <file_locking>true</file_locking>
            <read_control>true</read_control>
            <read_control_type>'crc32'</read_control_type>
            <hashed_directory_level>0</hashed_directory_level>
            <hashed_directory_umask>0700</hashed_directory_umask>
            <file_name_prefix>zend_cache</file_name_prefix>
            <cache_file_umask>0600</cache_file_umask>
            <metadatas_array_max_size>100</metadatas_array_max_size>
        </slow_backend_options>
</cache>
</global>

如果backend是apc(xcache和eaccelerator),不需要设置任何参数,也没有需要设置的参数。如果backend是sqlite,backend_options中的cache_db_complete_path必须给出数据库文件。如果backend是memcached,针对memcached的配置直接作为cache的子节点(不要放入backend_options中),这个可以配置的值可以参考它的类的_options:

    protected $_options = array(
        'servers' => array(array(
            'host' => self::DEFAULT_HOST,
            'port' => self::DEFAULT_PORT,
            'persistent' => self::DEFAULT_PERSISTENT,
            'weight'  => self::DEFAULT_WEIGHT,
            'timeout' => self::DEFAULT_TIMEOUT,
            'retry_interval' => self::DEFAULT_RETRY_INTERVAL,
            'status' => self::DEFAULT_STATUS,
            'failure_callback' => self::DEFAULT_FAILURE_CALLBACK
        )),
        'compression' => false,
        'compatibility' => false,
    );

举例如下配置:

<global>
	<cache>
            <memcached>
                <servers>
                    <server>
                        <host><![CDATA[]]></host>
                        <port><![CDATA[]]></port>
                        <persistent><![CDATA[]]></persistent>
                        <weight><![CDATA[]]></weight>
                        <timeout><![CDATA[]]></timeout>
                        <retry_interval><![CDATA[]]></retry_interval>
                        <status><![CDATA[]]></status>
                    </server>
                </servers>
                <compression><![CDATA[0]]></compression>
                <cache_dir><![CDATA[]]></cache_dir>
            </memcached>
        </cache>
</global>

如果Servers中包含了多个Server,则需要不同的名字包围。

配置文件:

        <cache>
       		<backend>memcached</backend>
          	<id_prefix>vfeelit_</id_prefix>
		<!-- 
		<backend_options></backend_options>
		-->
          	<memcached>
           		<servers>
             		<server_one>
                 		<host>127.0.0.1</host>
                   		<port>11222</port>
                     	        <weight>1</weight>
               		</server_one>
                	<server_two>
               			<host>127.0.0.1</host>
                      	        <port>11221</port>
                      	        <weight>1</weight>
              		</server_two>
          		</servers>
         	</memcached>

      		<slow_backend_options>
       			<cache_dir>/www/default/magento.vfeelit.com/public_html/var/cache</cache_dir>
          		<file_locking>true</file_locking>
            	<read_control>true</read_control>
            	<read_control_type>crc32</read_control_type>
          		<hashed_directory_level>2</hashed_directory_level>
           		<hashed_directory_umask>0700</hashed_directory_umask>
            	<file_name_prefix>vfeelit</file_name_prefix>
            	<cache_file_umask>0600</cache_file_umask>
          		<metadatas_array_max_size>100</metadatas_array_max_size>
      		</slow_backend_options>
        </cache>

直接抓取cache节点内容转换为数组:

Array
(
    [backend] => memcached
    [id_prefix] => vfeelit_
    [memcached] => Array
        (
            [servers] => Array
                (
                    [server_one] => Array
                        (
                            [host] => 127.0.0.1
                            [port] => 11222
                            [weight] => 1
                        )

                    [server_two] => Array
                        (
                            [host] => 127.0.0.1
                            [port] => 11221
                            [weight] => 1
                        )

                )

        )

    [slow_backend_options] => Array
        (
            [cache_dir] => /www/default/magento.vfeelit.com/public_html/var/cache
            [file_locking] => true
            [read_control] => true
            [read_control_type] => crc32
            [hashed_directory_level] => 2
            [hashed_directory_umask] => 0700
            [file_name_prefix] => vfeelit
            [cache_file_umask] => 0600
            [metadatas_array_max_size] => 100
        )

)

最终获取如下结果中的options传入后端:

Array
(
    [type] => TwoLevels
    [options] => Array
        (
            [fast_backend] => Libmemcached
            [fast_backend_options] => Array
                (
                    [servers] => Array
                        (
                            [server_one] => Array
                                (
                                    [host] => 127.0.0.1
                                    [port] => 11222
                                    [weight] => 1
                                )

                            [server_two] => Array
                                (
                                    [host] => 127.0.0.1
                                    [port] => 11221
                                    [weight] => 1
                                )

                        )

                )

            [fast_backend_custom_naming] => 1
            [fast_backend_autoload] => 1
            [slow_backend_custom_naming] => 1
            [slow_backend_autoload] => 1
            [auto_refresh_fast_cache] => 
            [slow_backend] => File
            [slow_backend_options] => Array
                (
                    [cache_dir] => /www/default/magento.vfeelit.com/public_html/var/cache
                    [file_locking] => true
                    [read_control] => true
                    [read_control_type] => crc32
                    [hashed_directory_level] => 2
                    [hashed_directory_umask] => 0700
                    [file_name_prefix] => vfeelit
                    [cache_file_umask] => 0600
                    [metadatas_array_max_size] => 100
                )

        )

)

fast_backend_options缓存传递到Libmemcached,这个是实例化对象需要的具体参数。

另外,传递到前端的是数据:

Array
(
    [caching] => 1
    [lifetime] => 7200
    [automatic_cleaning_factor] => 0
    [cache_id_prefix] => vfeelit_
)

永久链接:http://blog.ifeeline.com/742.html
原创文章,转载务必保留出处。

Magento会话共享改进(快慢两层存储)

如果使用了服务器集群,那么会话数据共享是必须实现的,因为请求进入的服务器可能不一样,但是它的会话ID是一样的,所以它的会话内容应该向各个应用服务器开放。用很多方法可以实现会话共享,比如可以在数据库存储服务器上划分一个区,然后各个应用服务器都mount这个区,会话都写入这里,理论上就能解决会话共享问题,或者直接保存到后端的数据库,每个应用都链接到后端数据库获取会话内容,这个实现也非常简单,如果数据库服务器压力大,可以进行主从复制读写分离。或者存入文件和存入数据库都不够快,那么可以存入到共享内存中,比如Memecached,不过存入共享内存有一个缺点,共享内存用完可能会产生问题,于是有了一个想法,能不能实现像缓存一样实现两层存储?

所谓两层,简单描述如下:
保存到Memcached中的同时也保存到数据库中,写入时监控共享内存利用率,如果超过伐值,从共享内存中删除这个会话,这时可能释放了共享内存,只保存到了数据库,而取数据时,先到共享内存取,如果没有命中则到数据库取。

经过研究,在Magento中只要重写Mage_Core_Model_Resource_Session类就能实现。

首先在自定义模块的配置文件中重写这个类:

<global>
        <models>
            <mysession>
                <class>Vfeelit_Mysession_Model</class>
            </mysession>
            <core_resource>
                <rewrite>
                     <session>Vfeelit_Mysession_Model_Resource_Session</session>
            	</rewrite>
            </core_resource>
        </models>
</global>

然后我们的Vfeelit_Mysession_Model_Resource_Session继承自Mage_Core_Model_Resource_Session。在构造函数中引入:

	public function __construct()
	{
		$mySession = Mage::getConfig()->getNode(self::XML_NODE_MYSESSION)->asArray();
		
		$prefix = $mySession['session_prefix'];
		if(!empty($prefix)){
			$this->_prefix = $prefix;
		}

		$options = $mySession['session_servers'];
		$this->_backend = new Zend_Cache_Backend_Libmemcached($options);

		parent::__construct();
	}

我这里实例化一个Zend_Cache_Backend_Libmemcached对象(PHP必须安装了Memcached扩展),用它来链接Memcached服务器,把数据保存到共享内存中。我这里还设置了一个key的前缀(因为我自己控制数据存入,所以key就可以自定义了),后面就是调用父类的构造函数,它负责数据库初始化。

要自定义会话的处理,需要使用session_set_save_handler()绑定6个方法,这个已经在Mage_Core_Model_Resource_Session中实现:

	public function setSaveHandler()
	{
		if ($this->hasConnection()) {
			session_set_save_handler(
				array($this, 'open'),
				array($this, 'close'),
				array($this, 'read'),
				array($this, 'write'),
				array($this, 'destroy'),
				array($this, 'gc')
			);
		} else {
			session_save_path(Mage::getBaseDir('session'));
		}
		return $this;
	}	

看起来,我们不需要动这个方法。我们需要重载的方法是read和write和destory(gc方法直接使用继承过来的即可)。

首先看read重载方法的实现:

	public function read($sessId)
	{
		if($this->_backend){
			$id = $this->_prefix.$sessId;
			$data = $this->_backend->load($id);
		}
		if(!$data){
			return parent::read($sessId);
		}else{
			return $data;
		}
	}

实际上非常简单,从共享内存中取数据,如果没有取到(可能是共享服务器down了或被存满了),就去数据库里取,从数据库取数据的逻辑Magento本身已经实现。

然后看write方法:

	public function write($sessId, $sessData)
	{
		if($this->_backend){
			$id = $this->_prefix.$sessId;
			$this->_save($sessData, $id, array(), $this->getLifeTime());
		}
		return parent::write($sessId, $sessData);
	}

这个方法同时保存到共享内存和数据库,这个是read方法无法读取到数据时可以从数据库里读取到的保证。这里我实现了一个_save方法抽象了保存的方法:

	private function _save($data, $id, $tags = array(), $specificLifetime = false)
	{
		$usage = $this->_getFastFillingPercentage('saving');
		$boolFast = true;
	
		if($specificLifetime){
			$lifetime = $specificLifetime;
		}else{
			$lifetime = $this->getLifetime();
		}
	
		//$preparedData = $this->_prepareData($data, $lifetime);

		if ( 85 >= $usage) {
			$boolFast = $this->_backend->save($data, $id, array(), time()+$lifetime);
		} else {
			$boolFast = $this->_backend->remove($id);
			if (!$boolFast && !$this->_backend->test($id)) {
				// some backends return false on remove() even if the key never existed. (and it won't if fast is full)
				// all we care about is that the key doesn't exist now
				$boolFast = true;
			}
	
		}
		return $boolFast;
	}

通过_getFastFillingPercentage方法获取共享内存的利用率,当利用率没有超过85,直接保存到共享内存中。如果超过了,就把这个KEY从数据库中踢掉,最后确保已经成功删除。这样,到达伐值后,就释放空间,内容从数据库获取,释放的空间和内存过期的会话释放的空间如果低于了伐值,新的会话数据库继续写入共享内存,从而实现了快慢存储。

Magento会话数据快慢存储

从测试来看,完成没有问题。另外一个需要主要的是,会话的共享内存空间最好不要和其它的缓存内存混用,因为会话内容共享内存一定要设置过期时间,方便垃圾数据及时退出,而缓存共享内存就不一定了,可以设置它保存很长时间,甚至直到手动刷新或重启Memcached服务时才被清理,所有缓存空间可能会被快速占用而不释放,那么会话内容可能无法存入,加速会话读取(或共享内存)的美梦就会泡汤。

这里的自己写的代码不多,但是要非常清楚的内容就非常多,比如会话机制的原理,共享内存Memcached工作原理,Magento的类重写,Magento对会话的封装过程等。

永久链接:http://blog.ifeeline.com/736.html
原创文章,转载务必保留出处。

Magento Session与Session共享(Memcached)

前台控制器从Mage_Core_Controller_Front_Action继承,后台控制器从Mage_Adminhtml_Controller_Action继承,而它们的父类都是Mage_Core_Controller_Varien_Action,这里面的preDispatch方法是每个控制器在调用它的方法前都被执行的。里面有初始化会话的代码:

$session = Mage::getSingleton('core/session', array('name' => $this->_sessionNamespace))->start();

这里的对应所在的具体对象不同,可能为fontend和adminhtml。实际上Mage_Adminhtml_Controller_Action覆盖了preDispatch方法,它实现了自己的逻辑。
Magento会话类层次

搜索:“extends Mage_Core_Model_Session_Abstract”发现有22个匹配。

Mage_Core_Model_Session的构造函数:

    public function __construct($data=array())
    {
        $name = isset($data['name']) ? $data['name'] : null;
        $this->init('core', $name);
    }

把传递进来的$name传递到init函数初始化,这个函数从它的父类Mage_Core_Model_Session_Abstract继承过来:

//Mage_Core_Model_Session_Abstract
	// $namespace->core  $sessionName->frontend
    public function init($namespace, $sessionName=null)
    {
        parent::init($namespace, $sessionName);
        $this->addHost(true);
        return $this;
    }

而这个方法中又调用它的父类的Mage_Core_Model_Session_Abstract_Varien方法:

    public function init($namespace, $sessionName=null)
{
		//启动会话,$sessionName -> frontend
        if (!isset($_SESSION)) {
            $this->start($sessionName);
        }
		//
        if (!isset($_SESSION[$namespace])) {
            $_SESSION[$namespace] = array();
        }

        $this->_data = &$_SESSION[$namespace];

        $this->validate();
        $this->revalidateCookie();

        return $this;
    }

这个方法如果没有设置$_SESSION(默认Magento把自动启动会话关闭),就可以调用start方法开启会话。实际上,全局这里只调用这一次(区分前后台),因为:

    public function start($sessionName=null)
    {
        if (isset($_SESSION)) {
            return $this;
        }
		//…
	}

start方法中,只要会话一开始,就直接返回。不过为了确保会话正常开始,在实例化Mage_Core_Model_Session时还是调用它的start方法。

接下来start中的这段代码:

        switch($this->getSessionSaveMethod()) {
            case 'db':
                ini_set('session.save_handler', 'user');
                $sessionResource = Mage::getResourceSingleton('core/session');
                /* @var $sessionResource Mage_Core_Model_Mysql4_Session */
                $sessionResource->setSaveHandler();
                break;
            case 'memcache':
                ini_set('session.save_handler', 'memcache');
                session_save_path($this->getSessionSavePath());
                break;
            case 'memcached':
                ini_set('session.save_handler', 'memcached');
                session_save_path($this->getSessionSavePath());
                break;
            case 'eaccelerator':
                ini_set('session.save_handler', 'eaccelerator');
                break;
            default:
                session_module_name($this->getSessionSaveMethod());
                if (is_writable($this->getSessionSavePath())) {
                    session_save_path($this->getSessionSavePath());
                }
                break;
        }

决定了存储会话数据的介质。在Magento安装的时候可以选择的是文件和数据库。第一种情况存储到数据库,那么就会使用到会话的资源模型(因为和数据库交互),如果是保存到文件中,那么路径可写是必须检查的。

我们这里看到很多的Session类实现,实际上,只是说明模块需要保留数据到会话而已,每个类继承过来的init方法的第一个参数是一个所谓的名空间,实际上是$_SESSION数组的一个下标,比如core对应$_SESSION[‘core’]=array(), customer对应$_SESSION[‘customer’]=array()。

实际上这里的封装也不复杂。关于SESSION的内容可以参考http://blog.ifeeline.com/300.html。

这里探讨一下存储到共享内存的方法。处了保存到数据库和文件,Magento支持把SESSION数据通过memeche和memcached和eaccelerator保存到共享内存中。这三个都是PHP的扩展库。需要安装才能用。

在Magento的app/etc/local.xml中添加如此代码:

<global>
<!— 保存到文件 -->
<session_save><![CDATA[files]]></session_save>

		<!—通过memcached扩展把数据保存到memcached服务端 -->
        <session_save><![CDATA[memcached]]></session_save>
        <session_save_path><![CDATA[127.0.0.1:11212]]></session_save_path>
</global>

注意这里的memcached不是memcache(没有d)。

可以参考:http://www.php.net/manual/zh/memcached.sessions.php。这里给出了一个最佳实践,SESSION使用的缓存最好不要和其它的缓存共享。

针对memcached的配置,还有一个可以配置的参数是session_save_limiter,如果不理解这个最好不要设置,默认是no-cache,用来控制页面的缓存,参考http://www.php.net/manual/zh/function.session-cache-limiter.php。

这样我们在服务器上开启memcached服务:

/usr/local/bin/memcached -d -m 32 -u root -l 127.0.0.1 -p 11212

Magento共享内存数据查看

可见,数据已经保存到memcached共享内存中。在共享内存中保存SESSION有一个问题,当访问量比较大,可能用完共享空间,共享空间用完会产生什么情况呢,剔除最老的SESSION还是拒绝存入都会产生问题,另外如果多个应用都保存到了内存空间,Key的前缀都是memc.sess.key,后面的串看起来总不能保存唯一(应该有产生碰撞的可能)。这个问题的解决是使用快慢存储,比如在保存到Memcached中的同时也保存到数据库中,写入时监控共享内存利用率,如果超过伐值,从共享内存中删除这个会话,这时可能释放了共享内存,只保存到了数据库,而取数据时,先到共享内存取,如果没有命中则到数据库取,从而解决了以上的问题。

一般,为了有效共享内存,会话存在时间不应该设置过长甚至设置成永不过期(时间设置得很大),常见的时间应该是低于30分钟(1800秒,Magento默认是3600),SESSION的过期时间就是根据当前时间加上这个生存时间设置的,每次把会话内存读出来,都会自动写回去,这个主要是为了更新时间戳,写入共享内存的也是如此,共享内存的每块数据都有时间戳,超时就会自动从内存剔除。

以下看看生存时间的代码:

    public function getLifeTime()
    {
        if (is_null($this->_lifeTime)) {
            $configNode = Mage::app()->getStore()->isAdmin() ?
                    'admin/security/session_cookie_lifetime' : 'web/cookie/cookie_lifetime';
            $this->_lifeTime = (int) Mage::getStoreConfig($configNode);

            if ($this->_lifeTime < 60) {
                $this->_lifeTime = ini_get('session.gc_maxlifetime');
            }

            if ($this->_lifeTime < 60) {
                $this->_lifeTime = 3600; //one hour
            }

            if ($this->_lifeTime > self::SEESION_MAX_COOKIE_LIFETIME) {
                $this->_lifeTime = self::SEESION_MAX_COOKIE_LIFETIME; // 100 years
            }
        }
        return $this->_lifeTime;
    }

前台的从web/cookie/cookie_lifetime取出值,看起来是设置cookie的时间,实际上,保存会话ID的这个cookie生存时间,应该是和会话的生存时间一样长的。不过,在其它应用中,可能不这样设置,保存会话ID的cookie生存时间可能会设置为0,这个表示这个cookie在浏览器关闭后马上过期(就是浏览器关闭的同时删除这个cookie),可见,虽然服务器端的会话内容还没有过期,但是这个数据已经作废了。重新打开浏览器访问,服务器只能给它一个新的会话(因为你没有把SID传过来,所以不知道你之前状态),这样服务器端就会产生很多作废数据。如果保存会话ID的这个cookie跟会话生存时间一样长,就不会因为浏览器关闭重新打开时重新开启一个会话,不过用户把浏览器关闭了很大情况可能时不想再浏览了,另外关闭浏览器cookie不过期还有一个问题时,如果另一个人在你的电脑上用同一浏览器打开了相同的站点,你的信息可能被暴露,所以我们通常会被教育,要记得登出。

永久链接:http://blog.ifeeline.com/732.html
原创文章,转载务必保留出处。

Magento缓存系统与Zend Framework缓存组件

Mage_Core_Model_Cache的构造函数有如下代码:

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

这里生成一个缓存对象,前端是Varien_Cache_Core,后端是$backend[‘type’]。
Zend Framework缓存系统

首先,Magento中实现了自己的前端Varien_Cache_Core,Varien_Cache_Core直接继承自Zend_Cache_Core,跟Zend Framework中的中Zend_Cache_Frontend_File、Zend_Cache_Frontend_File_Output、Zend_Cache_Frontend_Page等是一个层次的,本身它们都不太复杂,实际上是设置前端配置参数,比如缓存生存时间等,另一个就是调用后端保存/取回/清除缓存。实际上更多是使用Zend_Cache_Core提供的封装方法(Magento中还使用了一个Mage_Core_Model_Cache来封装前端操作)。

Zend Framework中缓存的后端有很多类型,每个后端都需要是实现Zend_Cache_Backend_ExtendedInte接口(这个即可又从Zend_Cache_Backend_Interface继承)和继承自Zend_Cache_Backend,Zend Framework中提供的缓存后端有:

Zend_Cache_Backend_File
Zend_Cache_Backend_Sqlite
Zend_Cache_Backend_Memcached
Zend_Cache_Backend_Apc
Zend_Cache_Backend_Xcache
Zend_Cache_Backend_ZendPlatform
Zend_Cache_Backend_Libmemcached
Zend_Cache_Backend_TwoLevels //封装其它后端,实现慢快保存

我们可以看到,ZF并没有提供针对Database和Eaccelerat的后端类型,所以Magento添加了这两种。另外,使用Varien_Cache_Backend_Memcached扩展了Zend_Cache_Backend_Memcached(从Magento源代码看来Varien_Cache_Backend_Memcached并没有在Magento中使用)。

我们可以从Magento的Mage_Core_Model_Cache的_getBackendOptions方法中知道,它支持的后端类型有sqlite、memcached、apc、xcache、eaccelerator、database、file。

缓存对象一般指前端对象(也可能使用另外一个对象进行封装),前端对象必定需要一个后端为它服务。一般不直接实例化前端对象,而是使用Zend_Cache的factory方法:

    public static function factory($frontend, $backend, $frontendOptions = array(), $backendOptions = array(), $customFrontendNaming = false, $customBackendNaming = false, $autoload = false)
    {
        if (is_string($backend)) {
            $backendObject = self::_makeBackend($backend, $backendOptions, $customBackendNaming, $autoload);
        } else {
        }
        if (is_string($frontend)) {
            $frontendObject = self::_makeFrontend($frontend, $frontendOptions, $customFrontendNaming, $autoload);
        } else {
        }
        $frontendObject->setBackend($backendObject);
        return $frontendObject;
    }

$frontend和$backend一般都是使用字符串(也可以传递具体的对象,必须是符合类型的),$frontendOptions和$backendOptions对应前端和后端需要使用参数,$customFrontendNaming和$customBackendNaming表示是否使用自定义的命名,$autoload表示当使用自定义的命名时,是否自定装载类。

在初始化后端时,调用了_makeBackend,它的$frontend是一个字符串,不管怎么样,首先匹配标准的后端,如果无法匹配,就根据$customBackendNaming进行类名组装,如果是ture,直接就把$frontend作为了类名。所以,如果使用自定义的后端类,需要把它设置为true并且要把全类名传递进来, $autoload为true还会检查类名是否可读,最终把$backendOptions传递到构造函数中,返回这个后端实例。_makeFrontend函数跟这个过程一样。

由于后端基本都实现Zend_Cache_Backend_ExtendedInterface和Zend_Cache_Backend_Interface接口:

// Zend_Cache_Backend_Interface
public function setDirectives($directives);
public function load($id, $doNotTestCacheValidity = false);
public function test($id);
public function save($data, $id, $tags = array(), $specificLifetime = false);
public function remove($id);
public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array());

//Zend_Cache_Backend_ExtendedInterface
public function getIds();
public function getTags();
public function getIdsMatchingTags($tags = array());
public function getIdsNotMatchingTags($tags = array());
public function getIdsMatchingAnyTags($tags = array());
public function getFillingPercentage();
public function getMetadatas($id);
public function touch($id, $extraLifetime); //修改时间
    public function getCapabilities();

Zend_Cache_Backend_Interface提供了最基本的保存、取回、清除缓存的方法。Zend_Cache_Backend_ExtendedInterface提供了管理缓存的基本方法。

后端还会继承Zend_Cache_Backend,它提供了后端一般实现,注意,后端是直接实现以上接口的,所有后端需要直接实现接口方法。比如setOption()选项保存到_options属性中。看它的构造函数:

    public function __construct(array $options = array())
    {
        while (list($name, $value) = each($options)) {
            $this->setOption($name, $value);
        }
    }

以下以文件后端(Zend_Cache_Backend_File)为例,探讨它的内部细节。首先看构造函数:

    public function __construct(array $options = array())
    {
        parent::__construct($options); //设置_options
        if ($this->_options['cache_dir'] !== null) { // particular case for this option
            $this->setCacheDir($this->_options['cache_dir']);
        } else {
            $this->setCacheDir(self::getTmpDir() . DIRECTORY_SEPARATOR, false);
        }
        if (isset($this->_options['file_name_prefix'])) { // particular case for this option
            if (!preg_match('~^[a-zA-Z0-9_]+$~D', $this->_options['file_name_prefix'])) {
                Zend_Cache::throwException('Invalid file_name_prefix : must use only [a-zA-Z0-9_]');
            }
        }
        if ($this->_options['metadatas_array_max_size'] < 10) {
            Zend_Cache::throwException('Invalid metadatas_array_max_size, must be > 10');
        }
        if (isset($options['hashed_directory_umask']) && is_string($options['hashed_directory_umask'])) {
            // See #ZF-4422
            $this->_options['hashed_directory_umask'] = octdec($this->_options['hashed_directory_umask']);
        }
        if (isset($options['cache_file_umask']) && is_string($options['cache_file_umask'])) {
            // See #ZF-4422
            $this->_options['cache_file_umask'] = octdec($this->_options['cache_file_umask']);
        }
    }

这里主要对Options值进行纠正。这个类内部有一个默认值:

    protected $_options = array(
        'cache_dir' => null, //缓存目录
        'file_locking' => true, //文件锁
        'read_control' => true, //读控制
        'read_control_type' => 'crc32', //读控制类型
        'hashed_directory_level' => 0, //采用几层目录存储
        'hashed_directory_umask' => 0700, //目录umask
        'file_name_prefix' => 'zend_cache', //文件名前缀
        'cache_file_umask' => 0600, //缓存文件umask
        'metadatas_array_max_size' => 100 //元数据数组最大大小
    );

从构造函数中可知,如果如果没有传递cache_dir,那么它自动去寻找系统临时目录。下面跟踪save方法的实现:

    public function save($data, $id, $tags = array(), $specificLifetime = false)
    {
        clearstatcache();
        $file = $this->_file($id);
        $path = $this->_path($id);
        if ($this->_options['hashed_directory_level'] > 0) {
            if (!is_writable($path)) {
                // maybe, we just have to build the directory structure
                $this->_recursiveMkdirAndChmod($id);
            }
            if (!is_writable($path)) {
                return false;
            }
        }
        if ($this->_options['read_control']) {
            $hash = $this->_hash($data, $this->_options['read_control_type']);
        } else {
            $hash = '';
        }
        $metadatas = array(
            'hash' => $hash,
            'mtime' => time(),
            'expire' => $this->_expireTime($this->getLifetime($specificLifetime)),
            'tags' => $tags
        );
        $res = $this->_setMetadatas($id, $metadatas);
        if (!$res) {
            $this->_log('Zend_Cache_Backend_File::save() / error on saving metadata');
            return false;
        }
        $res = $this->_filePutContents($file, $data);
        return $res;
    }

首先获取将要缓存的文件的完整路径,然后是文件前缀。举例来说,文件名为Test_Cache,路径为/tmp/, file_name_prefix设置为zen_cache:

//hashed_directory_level为2
/tmp/zen_cache--*/zen_cache--**/
/tmp/zen_cache--*/zen_cache--**/zen_cache---Test_Cache

#如果hashed_directory_level为1
/tmp/zen_cache--*/
/tmp/zen_cache--*/zen_cache---Test_Cache

#如果没有设置hashed_directory_level(默认为0)
/tmp/zen_cache/
/tmp/zen_cache/zen_cache---Test_Cache

注意这里的*代表一个字符,是对文件名字进行哈希得来的。文件的存储路径和名字已经确定,接下来可能需要创建目录(如果是多层目录存放,这个时候hashed_directory_umask设置会直接影响创建目录的权限)。然后是判断是否有读控制(默认是true),如果true就使用read_control_type指定的类型获取数据哈希(就是数据签名,用来作为缓存的元数据),然后设置元数据:

        $metadatas = array(
            'hash' => $hash,
            'mtime' => time(),
            'expire' => $this->_expireTime($this->getLifetime($specificLifetime)),
            'tags' => $tags
        );
        $res = $this->_setMetadatas($id, $metadatas);

如果跟踪下去,它和保存缓存文件的过程非常类似(在进入_setMetadatas时,会对$metadatas数组是否大于metadatas_array_max_size进行判断,如果大于就进行裁切)。这里需要提一下的,缓存文件是创建时间 和 过期时间是放入元数据文件中的。

最后:

$res = $this->_filePutContents($file, $data);
return $res;

把数据保存到文件,返回是否成功的状态。

然后简单看下load()方法:

    public function load($id, $doNotTestCacheValidity = false)
{
		//测试是否命中,$doNotTestCacheValidity控制是否检查缓存有没有过期,如果为false,则检查,也是默认值,这样可以避免很多问题
        if (!($this->_test($id, $doNotTestCacheValidity))) {
            // The cache is not hit !
            return false;
        }
        $metadatas = $this->_getMetadatas($id);
        $file = $this->_file($id);
        $data = $this->_fileGetContents($file);
		//如果设置了读控制,则会校验数据hash,如果读出来的数据的hash和元数据hash不一样,说明这个缓存有问题,记录日志后将它删除
        if ($this->_options['read_control']) {
            $hashData = $this->_hash($data, $this->_options['read_control_type']);
            $hashControl = $metadatas['hash'];
            if ($hashData != $hashControl) {
                // Problem detected by the read control !
                $this->_log('Zend_Cache_Backend_File::load() / read_control : stored hash and computed hash do not match');
                $this->remove($id);
                return false;
            }
        }
        return $data;
    }

读写文件需要用到file_locking和cache_file_umask的配置。

接下来看看remove

    public function remove($id)
    {
        $file = $this->_file($id);
        $boolRemove   = $this->_remove($file);
        $boolMetadata = $this->_delMetadatas($id);
        return $boolMetadata && $boolRemove;
}

根据ID把缓存文件和对应的元数据删除。除了这个方法,还提供了更加通用的方法:

    public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
    {
        // We use this protected method to hide the recursive stuff
        clearstatcache();
        return $this->_clean($this->_options['cache_dir'], $mode, $tags);
    }

Zend_Cache提供了预定义的几个模式:

    const CLEANING_MODE_ALL              = 'all';
    const CLEANING_MODE_OLD              = 'old';
    const CLEANING_MODE_MATCHING_TAG     = 'matchingTag';
    const CLEANING_MODE_NOT_MATCHING_TAG = 'notMatchingTag';
    const CLEANING_MODE_MATCHING_ANY_TAG = 'matchingAnyTag';

clean方法根据不同模式,第二参数需要给定Tag。

还有其它一些有用方法,比如touch, getIds, getTags, getMetadatas等,实际也不复杂,另外要注意的时候,获取缓存只能是用load()给出ID,不能使用tag来load,道理是很明白的。

不过这里讨论的后端的有用方法,一般都不是直接操作的,后端是为前端服务的,所有一般是用前端对象的方法(间接调用后端的方法),由于有很多不同类型的后端,所有引入前端的概念可以提供一个通用的方案。

在Magento中,对应了一个Mage_Core_Model_Cache的对象,它用来封装cache对象(它需要和数据库交互),App的_cache引用到这个对象,这个cache的_frontend属性引用的才是前端对象(Varien_Cache_Core),Mage_Core_Model_Cache的save方法对$tags数组中只要没有包含CONFIG这个tag,就统一在最后添加一个叫MAGE的tag。

Magento中的Mage_Core_Model_Cache对象的方法:

    protected function _getFrontendOptions(array $cacheOptions)
{
        $options = isset($cacheOptions['frontend_options']) ? $cacheOptions['frontend_options'] : array();
        if (!array_key_exists('caching', $options)) {
            $options['caching'] = true;
        }
        if (!array_key_exists('lifetime', $options)) {
            $options['lifetime'] = isset($cacheOptions['lifetime']) ? $cacheOptions['lifetime']
                : self::DEFAULT_LIFETIME;
        }
        if (!array_key_exists('automatic_cleaning_factor', $options)) {
            $options['automatic_cleaning_factor'] = 0;
        }
        $options['cache_id_prefix'] = $this->_idPrefix;
        return $options;
    }

这里把_idPrefix赋值给$options[‘cache_id_prefix’],这个参数会被传入前端(已经写死了),覆盖前端对象的cache_id_prefix值,这里的_idPrefix如果配置文件中没有指定id_prefix,就使用prefix,如果还没有指定就是 ../app/etc目录MD5后取前三个字符加下滑线。

在Zend_Cache_Core中在保存 和 取出 和 清除缓存时,都会把id经过_id()方法过滤

    protected function _id($id)
    {
        if (($id !== null) && isset($this->_options['cache_id_prefix'])) {
            return $this->_options['cache_id_prefix'] . $id; // return with prefix
        }
        return $id; // no prefix, just return the $id passed
    }

可以看到前端的_options的cache_id_prefix不为null,那么还会在id名前加上这个前缀。

我们可以从Zend_Cache_Core中获取可配置的前端参数:

    protected $_options = array(
        'write_control'             => true,
        'caching'                   => true,
        'cache_id_prefix'           => null,
        'automatic_serialization'   => false,
        'automatic_cleaning_factor' => 10,
        'lifetime'                  => 3600,
        'logging'                   => false,
        'logger'                    => null,
        'ignore_user_abort'         => false
    );

Mage_Core_Model_Cache在_getFrontendOptions方法根据global/cache/ frontend_options的配置,设置caching,lifetime,automatic_cleaning_factor,这些如果没有配置就是它的默认值,所以基本是写死了。注意cache_id_prefix是真写死。从而知道它的配置模式:

<global>
	<cache>
	<frontend_option>
	<caching />
	<lifetime />
<automatic_cleaning_factor />
</frontend_option>
</cache>
</global>

所以,你看到magento中的缓存文件:

mage---841_TEST_CACHE

就不需要奇怪了,TEST_CACHE是ID, 841_则是MD5前三个字符加下划线。mage是缓存前缀,这个只有后端是File时才有的:

//Mage_Core_Model_Cache
    protected $_defaultBackendOptions = array(
        'hashed_directory_level'    => 1,
        'hashed_directory_umask'    => 0777,
        'file_name_prefix'          => 'mage',
);

	//进入default时$backendType 没有设置, 这里就是指后端是File时,看到把$_defaultBackendOptions传递入后端类型,那么file_name_prefix就被设置了,另外,默认hashed_directory_level是0,这里改为了1
        if (!$backendType) {
            $backendType = $this->_defaultBackend;
            foreach ($this->_defaultBackendOptions as $option => $value) {
                if (!array_key_exists($option, $options)) {
                    $options[$option] = $value;
                }
            }
        }

hashed_directory_level是一层,file_name_prefix默认为mage,那么就产生这样的文件:

mage—1/mage---841_TEST_CACHE

你可以看到,以上那段代码是写死了的。意思是说,对于后端是File类型的,前缀必须是mage,层次只能是1,你可以把这里的1改成2,试手一下。

File的类型,很多参数都写死了。可用的参数:

    protected $_options = array(
        'cache_dir' => null, //写死为 /../var/cache
        'file_locking' => true,
        'read_control' => true,
        'read_control_type' => 'crc32',
        'hashed_directory_level' => 0, //写死为1
        'hashed_directory_umask' => 0700, //写死为0511
        'file_name_prefix' => 'zend_cache', //写死为mage
        'cache_file_umask' => 0600,
        'metadatas_array_max_size' => 100
    );

可以通过配置来改变File类型的后端参数不多:

<global>
	<cache>
<backend>File</backend>
<id_prefix></id_prefix>
			<backend_options>
				<cache_file_umask>0700</cache_file_umask>
</backend_options>
</cache>
</global>

不过你可以指定id_prefix,如果没有指定id_prefix就使用prefix作为id_prefix,否则就系统自动生成。prefix是id_prefix的候补(id_prefix更加明确,应该使用这个设置缓存名字的前缀),除此,没有其它作用。(在源码中,prefix没有传递到后端模型)

backend如果是File就可以省略。如果是其它类型就需要指定,比如APC,由于它是共享内存,一个名字对应一份缓存,所以这时候指定id_frefix非常有必要(如果是文件,存储路径会由于每个应用不同而不同),比如有一个叫tCache保存到了APC中,如果其它应用程序也设置了一个叫tCache的放入内存中,这样新的将覆盖旧的,那么你可能面临莫名其妙的错误。

另外,APC不支持tag。也没有backend_options需要设置。简单说就是backend指定为APC,id_prefix指定为缓存名前缀,Magento中的配置就完成了。

能够搞明白文件保存缓存的过程,对应比如apc等就很容易明白了。尽管ZF提供的实例化和管理缓存的方法已经很好了,但是Magento本身还是提供了一个自己的缓存抽象,它封装了自己的实现,实际上它是利用ZF缓存机制的非常成功的例子。不过,Magento最通用使用缓存的方法还不是直接操作它的Cache方法,通用的方法由Mage_Core_Model_App中提供:

public function getCacheInstance()
public function getCache()
public function loadCache($id)
public function saveCache($data, $id, $tags=array(), $lifeTime=false)
public function removeCache($id)
public function cleanCache($tags=array())

可以调用getCacheInstance获取Mage_Core_Model_Cache获取实例引用,然后通过它直接操作(它有save,load,remove, clear方法)。不过在Magento中只要记住通用方法即可。

永久链接:http://blog.ifeeline.com/727.html
原创文章,转载务必保留出处。

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

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

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

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

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

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

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

下面我们运行如下代码:

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

发现,它输出:

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

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

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

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

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

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

    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的构造函数:

    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,对应这个存储类型的选项。具体需要继续跟踪进入:

   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配置的影响),可以推导出配置结构:

<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时),所以最终返回的数据:

$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节点,设置是否缓存 和 缓存时间,参考以上给出的配置文件。

接下执行:

$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方法中,看如下代码:

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

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

    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如何被设置的,好吧,在构造函数中,刚才少说了如下代码:

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

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

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

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

如果进入System->Configuration->ADVANCED->System,你将看到:
Magento后台设置全页缓存
这个配置和我这里说的全页缓存不是一个概念。这里的zend_page_cache是Zend Server提供的一个扩展,具体怎么搞就没有研究了。
Zend_Page_Cahce配置
这个探讨先到这里。我们继续回到App的_initModules()方法中:

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

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

    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方法检查是否启用了缓存:

//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缓存管理

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

回到saveCache方法:

//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方法:

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

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

//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标识在使用缓存:

// 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只是一部分,这才解释了本文刚开始遇到的疑问。可是新疑问又来了,既然是部分配置,那么如何获取店铺的配置呢?

首先是获取配置的用法:

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

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

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

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

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

来一段测试程序:

		$data = "12345678901234567890000000";
		$id = "Test_Cache";
		$tags = array('xTest','xCache');
		
		$fromCache = Mage::app()->loadCache($id);
		if($fromCache){
			echo "From cache---><br />";
			echo $fromCache;
			Mage::app()->removeCache($id);
		}else{
			echo "No cache ---><br />";
			Mage::app()->saveCache($data, $id, $tags);
		}

Magento缓存文件

Magento缓存文件内容

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

永久链接:http://blog.ifeeline.com/715.html
原创文章,转载务必保留出处。

Magento的全局变量设计模式

PHP中,变量有全局的概念,比如$_GET $_POST等数组中保存的值都是全局的,如果直接使用一个变量,那么它也是全局可用的,如果在函数中使用global关键字声明一个变量,那这个函数就可以使用来自外部的这个变量值,函数中的这个变量值还可以从函数带出到外部,所有直接声明的变量或用global关键字声明的变量,实际上全部注册到$GLOBALS数组中。比如声明了$var,当要引用这个变量时可以直接用$var或$GLOBALS[‘var’]。这种搞法其实已经非常怪异了,不过还有更加怪异的,比如$_GET[‘name’],你可能可以使用$name直接引用或用$GLOBALS[‘name’]来引用这个变量(具体决定于PHP的配置,不过一般都是关闭的了)。 这些诡异的用法是PHP的历史原因导致的。不过作为一个PHP程序员,仍然需要知道这个。

这里的全局变量的使用有很大风险,比如我声明了一个$var,然后在其它地方再次声明一个同名的$var,那么后面的将悄悄覆盖前面的。更加糟糕的是,如果这个变量引用一个对象,但是由于一些第三方代码的引入,也声明了一个同名的变量,那么这个变量在其它地方调用方法时将出现错误,这个问题很令人烦恼。所以我们得到的教训是:慎用全局变量。

以上说得情况在面向过程的代码中中,尤为明显。由于这个问题,全局变量设计模式就产生了。以下主要探讨Magento中的全局设计模式和单态设计模式。

Magento中使用如下方法实现了全局设计模式:

Mage::register
Mage::unregister   
Mage::registry 

分别对应注册 和 注销 和 获取全局变量。首先看看注册一个全局变量时发生了什么事情:

    public static function register($key, $value, $graceful = false)
    {
        if (isset(self::$_registry[$key])) {
            if ($graceful) {
                return;
            }
            self::throwException('Mage registry key "'.$key.'" already exists');
        }
        self::$_registry[$key] = $value;
    }

首先检查$key是否存在$_registry数组中,如果存在就根据$graceful的设置,返回空或者抛出异常。如果不存在就直接赋值。可见,一个变量注册了,就不能再注册一个同名的变量了,这个很好解决了变量被覆盖的威胁。但是还是有不足,因为程序员可以先调用unregister来注销变量,然后设置自己的变量:

    public static function unregister($key)
    {
        if (isset(self::$_registry[$key])) {
            if (is_object(self::$_registry[$key]) && (method_exists(self::$_registry[$key], '__destruct'))) {
                self::$_registry[$key]->__destruct();
            }
            unset(self::$_registry[$key]);
        }
    }

注销全局变量时如果全局变量是对象并且设置了__destruct析构函数,那么直接调用它的析构函数释放资源。否则就是简单unset,这个就依赖PHP的垃圾回收器来回收了。

当想要获取这个全局变量时,直接调用registry,可以把它看做是注册表:

    public static function registry($key)
    {
        if (isset(self::$_registry[$key])) {
            return self::$_registry[$key];
        }
        return null;
    }

Magento内核高度使用这个模式,我们知道Magento中的控制器并不会直接和视图交互,真正和视图交互的是Block,控制器中只要使用renderLayout就可以获取输出:

$this->loadLayout();
$this->getLayout()->getBlock('content')->setSomeVar($var);
$this->renderLayout();

我们可以在控制器中添加:

Mage::register('product', $product);

一个Block中可能包含如下方法:

public function getProduct()
{
    return Mage::registry('product');
}

这样就可以在视图中使用:

echo $this->getProduct()->getName();


Magento中的单态设计模式(Singleton Pattern)

获取一个对象的一份单态实例:

Mage::getSingleton('group/class');

过多解释在面对代码时非常苍白,所以我们这里直接看看这个方法的实现:

#File: app/Mage.php
public static function getSingleton($modelClass='', array $arguments=array())
{
    $registryKey = '_singleton/'.$modelClass;
    if (!self::registry($registryKey)) {
        self::register($registryKey, self::getModel($modelClass, $arguments));
    }
    return self::registry($registryKey);
}

它使用’_singleton/’.$modelClass组建名字,然后调用register把getModel()返回的实例记录到注册表中,你可以看到,如果这个名称对应的值存在了,直接就放回这个实例,就是这样保证了全局唯一(看起来不是严格的单态模式,因为可以unregister)。这个封装另一个模式来实现的别的设计模式,也算是一个亮点吧。不过一般单态设计模式是控制类的构造函数来保证的,常见的使用方法是:

$bar = Object_Type::getInstance();

可是Magento中并没有采用这种方式。尽管可以unregister,但是它的灵活性也体现出来了。

永久链接:http://blog.ifeeline.com/710.html
原创文章,转载务必保留出处。

Magento中的404页面

HTTP的404状态码表示访问的链接不存在,这个状态码可能是由HTTP服务器直接返回的,也可能是由程序控制返回的。首先看看HTTP服务器返回的404。

在Magento跟目录下的.htaccess文件中有如下配置:

<IfModule mod_rewrite.c>

############################################
## enable rewrites

    Options +FollowSymLinks
    RewriteEngine on
############################################
## always send 404 on missing files in these folders

    RewriteCond %{REQUEST_URI} !^/(media|skin|js)/

############################################
## never rewrite for existing files, directories and links

    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-l

############################################
## rewrite everything else to index.php

    RewriteRule .* index.php [L]

</IfModule>

根据配置可知,当访问media|skin|js目录中不存在的页面文件时,httpd(Apache)将返回自身404设置,一般都是一个白底黑字的说明。可以修改默认处理:

// httpd
ErrorDocument 404 404.html
ErrorDocument 403 403.html

// nginx
fastcgi_intercept_errors on;
error_page 404 404.html;
error_page 403 403.html;

针对Nginx的Magento配置,参考:http://www.vfeelit.com/175.html

除了http服务的默认404处理,Magento本身也会可能做出404响应,查看Mage:run()方法代码:

  public static function run($code = '', $type = 'store', $options = array())
    {
        try {
//             throw new Mage_Core_Model_Store_Exception('');
        } catch (Mage_Core_Model_Session_Exception $e) {

        } catch (Mage_Core_Model_Store_Exception $e) {
            require_once(self::getBaseDir() . DS . 'errors' . DS . '404.php');
            die();
        } catch (Exception $e) {
    }

当捕捉到Mage_Core_Model_Store_Exception异常时,会返回errors/404.php页面,在try中主动抛出这个异常(以上代码注释部分),看看结果:
Magento 404 页面
实际上它直接把error目录下的404.php作为模板返回,具体细节就不在这里讨论了。不过比较感兴趣的是,这个异常是怎么被抛出的,在什么地方?

我们可以直接搜索Mage_Core_Model_Store_Exception类,最终是在App的getStore方法中找到:

    public function getStore($id = null)
    {
        if (!Mage::isInstalled() || $this->getUpdateMode()) {
            return $this->_getDefaultStore();
        }

        if ($id === true && $this->isSingleStoreMode()) {
            return $this->_store;
        }

        if (!isset($id) || ''===$id || $id === true) {
            $id = $this->_currentStore;
        }
        if ($id instanceof Mage_Core_Model_Store) {
            return $id;
        }
        if (!isset($id)) {
            $this->throwStoreException();
        }

        if (empty($this->_stores[$id])) {
            $store = Mage::getModel('core/store');
            /* @var $store Mage_Core_Model_Store */
            if (is_numeric($id)) {
                $store->load($id);
            } elseif (is_string($id)) {
                $store->load($id, 'code');
            }

            if (!$store->getCode()) {
                $this->throwStoreException();
            }
            $this->_stores[$store->getStoreId()] = $store;
            $this->_stores[$store->getCode()] = $store;
        }
        return $this->_stores[$id];
    }

如果无法取得店铺Id或者获取了Id但是无法根据Id获取店铺代码,这个异常就会被抛出,如果是这个情况那就是这个店铺根本不存在,访问的店铺都不存在自然要返回404了。不过由于Magento的逻辑,一般还不容易遇到这个页面返回。

接下来探讨Magento的第二种404,也是最常见的。可以参考http://blog.ifeeline.com/470.html了解Magento的路由过程。对于不存在frontName,控制器和action方法,一般都会路由到cms/index/noRoute(默认是这样,后台可配置),以下是default路由器的match方法,它展示了如何实现这个noRoute:

    public function match(Zend_Controller_Request_Http $request)
    {
        $noRoute        = explode('/', Mage::app()->getStore()->getConfig('web/default/no_route'));
/*       
   <default>
            <cms_home_page>home</cms_home_page>
            <cms_no_route>no-route</cms_no_route>
            <cms_no_cookies>enable-cookies</cms_no_cookies>
            <front>cms</front>
            <no_route>cms/index/noRoute</no_route>
            <show_cms_breadcrumbs>1</show_cms_breadcrumbs>
    </default>
    模块名被修改为了CMS 对应index控制器  和 noRoute方法,noRoute方法就是处理404返回的逻辑
*/
         
        $moduleName     = isset($noRoute[0]) ? $noRoute[0] : 'core';
        $controllerName = isset($noRoute[1]) ? $noRoute[1] : 'index';
        $actionName     = isset($noRoute[2]) ? $noRoute[2] : 'index';
 
        if (Mage::app()->getStore()->isAdmin()) {
            $adminFrontName = (string)Mage::getConfig()->getNode('admin/routers/adminhtml/args/frontName');
            if ($adminFrontName != $moduleName) {
                $moduleName     = 'core';
                $controllerName = 'index';
                $actionName     = 'noRoute';
                Mage::app()->setCurrentStore(Mage::app()->getDefaultStoreView());
            }
        }
 
        $request->setModuleName($moduleName)
            ->setControllerName($controllerName)
            ->setActionName($actionName);
 
        return true;
    }

从web/default/no_route配置(后台可配置)中获取cms/index/noRoute,对应填入模块名,控制器名和action名,很明显,如果没有找到就使用Mage_Cms_IndexController控制器的noRoute方法来处理:

    public function noRouteAction($coreRoute = null)
    {
        $this->getResponse()->setHeader('HTTP/1.1','404 Not Found');
        $this->getResponse()->setHeader('Status','404 File not found');

        $pageId = Mage::getStoreConfig(Mage_Cms_Helper_Page::XML_PATH_NO_ROUTE_PAGE);
        if (!Mage::helper('cms/page')->renderPage($this, $pageId)) {
            $this->_forward('defaultNoRoute');
        }
    }

Mage_Cms_Helper_Page::XML_PATH_NO_ROUTE_PAGE的值为web/default/cms_no_route,一般配置应该为no-route,接着就渲染这个no-route页面。这里先去后台看看配置:
Magento 404 页面设置
接着进入Cms的Page:
Magento Cms noroute
可以看到,这个404页面就是一个Cms Page,可以在这里进行随意修改定制。回到代码,万一没有为roRoute指定页面,那么就调用defaultNoRoute来处理:

    public function defaultNoRouteAction()
    {
        $this->getResponse()->setHeader('HTTP/1.1','404 Not Found');
        $this->getResponse()->setHeader('Status','404 File not found');

        $this->loadLayout();
        $this->renderLayout();
    }

可看到,它直接渲染输出:
Magento默认404页面输出
注意看,它还是输出了一句话,这个defaultNoRoute方法实际应用了cms.xml配置中的

    <cms_index_noroute translate="label">
        <label>CMS No-Route Page</label>
    </cms_index_noroute>

    <cms_index_defaultnoroute>
        <remove name="right"/>
        <remove name="left"/>
        
        <reference name="root">
            <action method="setTemplate"><template>page/1column.phtml</template></action>
        </reference>
        <reference name="content">
            <block type="core/template" name="default_no_route" template="cms/default/no-route.phtml"/>
        </reference>
    </cms_index_defaultnoroute>

cms_index_defaultnoroute的句柄配置,它应用了1column.phtml布局,然后在content中添加一个子块,这个子块对应cms/default/no-route.phtml模板,这个模板中只有“There was no 404 CMS page configured or found.”这段文本。

如果不想用Cms的页面,那么就可以自定义这个默认的模板获取一个友好的404页面输出。甚至,可以不使用Cms模块提供的noRroute方法,比如你希望使用一个自定义的模块来处理No Route页面以实现比较复杂的逻辑,那么可以在后台修改Default No-route URL的设置,这个设置会影响到Default路由器的赋值。

永久链接:http://blog.ifeeline.com/700.html
原创文章,转载务必保留出处。

Magento开发者模式(PHP的错误处理应用)

Magento中有一个叫开发者模式的设置。先记着这个。我们先看看错误处理程序设置的进入点,在index.php文件中有:

error_reporting(E_ALL | E_STRICT);
//接下来有一行注释的代码
#ini_set('display_errors', 1);

首先修改了错误报告的类型,我们知道,如果使用自定义错误处理程序,根本不受它的影响(指error_reporting设置的错误级别)。接下来的代码是设置是否显示错误,这应该是在没有使用自定义错误处理程序时配合使用的,要不然这两句代码大可以清理的了。

我们的应用从App的run方法或init方法开始,run方法调用baseInit,baseInit调用_initEnvironment,_initEnvironment内又调用setErrorHandler(self::DEFAULT_ERROR_HANDLER)方法:

//setErrorHandler(self::DEFAULT_ERROR_HANDLER)  DEFAULT_ERROR_HANDLER->mageCoreErrorHandler
    public function setErrorHandler($handler)
    {
        set_error_handler($handler);
        return $this;
    }

可见,直接使用set_error_handler函数直接设置错误处理程序为mageCoreErrorHandler,这个函数在app/code/core/Mage/Core/functions.php中定义。

function mageCoreErrorHandler($errno, $errstr, $errfile, $errline){
    //...
    $errorMessage .= ": {$errstr}  in {$errfile} on line {$errline}";
    if (Mage::getIsDeveloperMode()) {
        throw new Exception($errorMessage);
    } else {
        Mage::log($errorMessage, Zend_Log::ERR);
    }
}

这里省了一段代码,它罗列的所有错误代码,然后组建一个字符串$errorMessage,然后根据是否是开发模式,如果是就直接抛出异常,否则就把它记录到日志中。如果是记录到日志中,程序继续运行,现在问题是,如果抛出异常,谁来捕获这个异常,如何处理,程序是否继续运行?

这个需要回到App的run方法的包装函数Mage::run()函数中回答这个问题:

   public static function run($code = '', $type = 'store', $options = array())
    {
        try {
            // .....
            self::$_app->run(array(
                'scope_code' => $code,
                'scope_type' => $type,
                'options'    => $options,
            ));
        } catch (Mage_Core_Model_Session_Exception $e) {
            header('Location: ' . self::getBaseUrl());
            die();
        } catch (Mage_Core_Model_Store_Exception $e) {
            require_once(self::getBaseDir() . DS . 'errors' . DS . '404.php');
            die();
        } catch (Exception $e) {
            if (self::isInstalled() || self::$_isDownloader) {
                self::printException($e);
                exit();
            }
            try {
                self::dispatchEvent('mage_run_exception', array('exception' => $e));
                if (!headers_sent()) {
                    header('Location:' . self::getUrl('install'));
                } else {
                    self::printException($e);
                }
            } catch (Exception $ne) {
                self::printException($ne, $e->getMessage());
            }
        }
    }

很明显,如果自定义错误处理程序抛出异常,就在这里被捕获处理,有些异常是给出链接,有些则是打印输出。这意味,要是打开开发者模式,可以直接查看到错误的输出。

在index.php中有如下代码:

if (isset($_SERVER['MAGE_IS_DEVELOPER_MODE'])) {
    Mage::setIsDeveloperMode(true);
}

两个办法让这个代码生效,把条件去掉,或者在配置中设置MAGE_IS_DEVELOPER_MODE这个变量(一般直接去掉条件快速有实在)。

另外,在自定义的错误处理程序中,如果不是开发模式则不会抛出异常,错误就被记录到日志中,这个路径可以在后台配置:
System->Configuration->Developer->Log Settings
Magento错误日志

在调用Mage::log()函数时记录到System Log File Name,调用Mage::logException时记录到Exceptions Log File Name,实际上logException是log()函数的包装器,只是指定了不同的名字。

到此已经讨论了大部分内容了,我们需要谨记,仅仅依靠Magento中的自定义错误处理程序还是不够的,在开发时务必在PHP中做配置以快速找到错误。而在Magento开启开发者模式则可以直接输出异常信息,另外,可以有效利用Mage:log()来调试程序,它不受配置的影响,可以让它输出到自己期望的地方去。

继续查看在事件触发回调函数调用时的代码:

#File: app/code/core/Mage/Core/Model/App.php
protected function _callObserverMethod($object, $method, $observer)
{
    if (method_exists($object, $method)) {
        $object->$method($observer);
    } elseif (Mage::getIsDeveloperMode()) {
        Mage::throwException('Method "'.$method.'" is not defined in "'.get_class($object).'"');
    }
    return $this;
}

当回调函数不存在时,如果在开发模式下,就会抛出异常,这个可以让我们知道哪些回调函数不存在。

Magento中通过使用自定义的错误处理出现,把PHP的错误变成抛出异常。

永久链接:http://blog.ifeeline.com/690.html
原创文章,转载务必保留出处。