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

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

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

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

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

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

<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。在构造函数中引入:

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

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中实现:

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

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重载方法的实现:

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

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

1
2
3
4
5
6
7
8
9

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方法抽象了保存的方法:

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

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会话数据快慢存储

Magento会话数据快慢存储

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

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

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