2015-02-24 46 views
2

我有一個我最初使用基於文件的會話編寫的CherryPy Web應用程序。我經常會在會話中存儲潛在的大對象,例如運行報表的結果 - 我提供了以各種格式下載報表結果的選項,並且我不希望重新運行查詢。由於可能獲得不同的數據,用戶選擇下載。在使用基於文件的會話時,這工作得很好。CherryPy會話和大型對象?

現在我正在研究將第二臺服務器聯機的潛力,因此我需要能夠在服務器之間共享會話數據,對於這種情況,看起來使用memchached會話存儲類型是最合適的。我簡單地看了一下使用PostgreSQL存儲類型,但是這個選項很不完善,從我所能找到的結果來看,可能會被打破。所以我實現了memcached選項。

但是,現在我遇到了一個問題,當我嘗試將某些對象保存到會話中時,出現「AssertionError:Session id data for id xxx not set」。我假設這是由於對象大小超過CherryPy會話後端或memcached中設置的任意限制,但我不知道,因爲異常不告訴我爲什麼它沒有設置。我已經將memcached中的對象大小限制增加到最大128MB,以查看是否有幫助,但它沒有 - 但這可能不是一個安全選項。

那麼我的解決方案是什麼?有什麼辦法可以使用memcached會話存儲來存儲任意大的對象嗎?我是否需要爲這些對象「滾動我自己的」數據庫或類似的解決方案?問題可能不是基於尺寸的嗎?還是有另一種選擇,我錯過了?

回答

2

我使用mysql來處理我的cherrypy會話。只要對象是可序列化的(可以被醃製),您可以將它作爲blob(二進制大對象)存儲在mysql中。這裏是你想使用MySQL的會話存儲的代碼...

https://bitbucket-assetroot.s3.amazonaws.com/Lawouach/cherrypy/20111008/936/mysqlsession.py?Signature=gDmkOlAduvIZS4WHM2OVgh1WVuU%3D&Expires=1424822438&AWSAccessKeyId=0EMWEFSGA12Z1HF1TZ82

""" 
MySQLdb session module for CherryPy by Ken Kinder <http://kenkinder.com/> 

Version 0.3, Released June 24, 2000. 

Copyright (c) 2008-2009, Ken Kinder 
All rights reserved. 

Redistribution and use in source and binary forms, with or without 
modification, are permitted provided that the following conditions are met: 

    * Redistributions of source code must retain the above copyright notice, 
    this list of conditions and the following disclaimer. 

    * Redistributions in binary form must reproduce the above copyright 
    notice, this list of conditions and the following disclaimer in the 
    documentation and/or other materials provided with the distribution. 

    * Neither the name of the Ken Kinder nor the names of its contributors 
    may be used to endorse or promote products derived from this software 
    without specific prior written permission. 

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
""" 
import MySQLdb 
import cPickle as pickle 
import cherrypy 
import logging 
import threading 

__version__ = '0.2' 

logger = logging.getLogger('Session') 

class MySQLSession(cherrypy.lib.sessions.Session): 
    ## 
    ## These can be over-ridden by config file 
    table_name = 'web_session' 
    connect_arguments = {} 

    SCHEMA = """create table if not exists %s (
      id varchar(40), 
      data text, 
      expiration_time timestamp 
     ) ENGINE=InnoDB;""" 

    _database = None 

    def __init__(self, id=None, **kwargs): 
     logger.debug('Initializing MySQLSession with %r' % kwargs) 
     for k, v in kwargs.items(): 
      setattr(MySQLSession, k, v) 

     self.db = self.get_db() 
     self.cursor = self.db.cursor() 

     super(MySQLSession, self).__init__(id, **kwargs) 

    @classmethod 
    def get_db(cls): 
     ## 
     ## Use thread-local connections 
     local = threading.local() 
     if hasattr(local, 'db'): 
      return local.db 
     else: 
      logger.debug("Connecting to %r" % cls.connect_arguments) 
      db = MySQLdb.connect(**cls.connect_arguments) 
      cursor = db.cursor() 
      cursor.execute(cls.SCHEMA % cls.table_name) 
      db.commit() 
      local.db = db 

      return db 

    def _load(self): 
     logger.debug('_load %r' % self) 
     # Select session data from table 
     self.cursor.execute('select data, expiration_time from %s ' 
          'where id = %%s' % MySQLSession.table_name, (self.id,)) 
     row = self.cursor.fetchone() 
     if row: 
      (pickled_data, expiration_time) = row 
      data = pickle.loads(pickled_data) 

      return data, expiration_time 
     else: 
      return None 

    def _save(self, expiration_time): 
     logger.debug('_save %r' % self) 
     pickled_data = pickle.dumps(self._data) 

     self.cursor.execute('select count(*) from %s where id = %%s and expiration_time > now()' % MySQLSession.table_name, (self.id,)) 
     (count,) = self.cursor.fetchone() 
     if count: 
      self.cursor.execute('update %s set data = %%s, ' 
           'expiration_time = %%s where id = %%s' % MySQLSession.table_name, 
           (pickled_data, expiration_time, self.id)) 
     else: 
      self.cursor.execute('insert into %s (data, expiration_time, id) values (%%s, %%s, %%s)' % MySQLSession.table_name, 
           (pickled_data, expiration_time, self.id)) 
     self.db.commit() 

    def acquire_lock(self): 
     logger.debug('acquire_lock %r' % self) 
     self.locked = True 
     self.cursor.execute('select id from %s where id = %%s for update' % MySQLSession.table_name, 
          (self.id,)) 
     self.db.commit() 

    def release_lock(self): 
     logger.debug('release_lock %r' % self) 
     self.locked = False 
     self.db.commit() 

    def clean_up(self): 
     logger.debug('clean_up %r' % self) 
     self.cursor.execute('delete from %s where expiration_time < now()' % MySQLSession.table_name) 
     self.db.commit() 

    def _delete(self): 
     logger.debug('_delete %r' % self) 
     self.cursor.execute('delete from %s where id=%%s' % MySQLSession.table_name, (self.id,)) 
     self.db.commit() 

    def _exists(self): 
     # Select session data from table 
     self.cursor.execute('select count(*) from %s ' 
          'where id = %%s and expiration_time > now()' % MySQLSession.table_name, (self.id,)) 
     (count,) = self.cursor.fetchone() 
     logger.debug('_exists %r (%r)' % (self, bool(count))) 
     return bool(count) 

    def __del__(self): 
     logger.debug('__del__ %r' % self) 
     self.db.commit() 
     self.db.close() 
     self.db = None 

    def __repr__(self): 
     return '<MySQLSession %r>' % (self.id,) 

cherrypy.lib.sessions.MysqlSession = MySQLSession 

那麼你webapp.py會是這個樣子......

from mysqlsession import MySQLSession 
import cherrypy 
import logging 

logging.basicConfig(level=logging.DEBUG) 

sessionInfo = { 
    'tools.sessions.on': True, 
    'tools.sessions.storage_type': "Mysql", 
    'tools.sessions.connect_arguments': {'db': 'sessions'}, 
    'tools.sessions.table_name': 'session' 
} 

cherrypy.config.update(sessionInfo) 

class HelloWorld: 
    def index(self): 
     v = cherrypy.session.get('v', 1) 
     cherrypy.session['v'] = v+1 
     return "Hello world! %s" % v 

    index.exposed = True 

cherrypy.quickstart(HelloWorld()) 

如果您需要把一些對象在那裏做這樣的事情...

import pickle 

    pickledThing = pickle.dumps(YourObject.GetItems(), protocol=0, fix_imports=False) 

Hope thi s幫助!

+0

您的代碼鏈接不起作用,但從我在這裏可以看到的情況來看,這將非常適合我的目的,只需稍作調整即可使其適用於PostgreSQL而不是MySQL。謝謝! – ibrewster 2015-02-25 17:17:23

1

聽起來就像你想存儲對存儲在Memcache中的對象的引用,然後在需要時將其拉回,而不是依賴狀態來處理加載/保存。

+0

好的,這聽起來像一個好的開始,但留下了問題a)我如何存儲對象,以便服務器可以訪問它(db可能?)和b)如何處理擺脫對象時會話過期? – ibrewster 2015-02-24 21:09:17

1

從你的解釋我可以得出結論,從概念上說,混合用戶會話和緩存並不是一個好主意。主要設計的會話是持有用戶身份的狀態。因此它具有安全措施,鎖定,避免同時發生的變化等方面。會話存儲通常也是不穩定的。因此,如果你的意思是使用會話作爲緩存,你應該瞭解會話是如何工作的,結果如何。

我建議您這樣做,以建立正常的緩存您的域模型,生成報告數據並保持會話身份。

CherryPy細節

默認CherryPy會話實現鎖定會話數據。在OLAP情況下,用戶很可能無法執行併發請求(例如打開另一個標籤),直到報告完成。然而,有一個手動鎖定管理的選項。

PostgreSQL會話存儲已損壞,可能會在下一個版本中刪除。

Memcached會話存儲未實現分佈式鎖定,因此請確保使用一致的規則來平衡您的服務器上的用戶。

+0

關於使用通用「緩存」而不是會話數據的事情是報告結果是a)用戶特定的,b)臨時的。使用會話對象會自動爲我提供這兩種功能。使用別的東西,我必須自己實現它們 - 這很好,如果這是做事情的最佳方式,但是如果不是這樣的話,還是要做很多額外的工作。 – ibrewster 2015-02-25 17:12:36

+0

@ibrewster對於您的要求a)將userId添加到密鑰b)爲緩存條目設置TTL。只要您瞭解其語義,就可以使用會話存儲。是的,它會自動發佈,但它不是免費的。我提供了細節。順便說一句,有許多Python緩存庫緩存就像在您的方法中應用裝飾器一樣簡單。 – saaj 2015-02-25 17:57:42