【问题标题】:CherryPy Sessions and large objects?CherryPy 会话和大型对象?
【发布时间】:2015-02-24 20:30:20
【问题描述】:

我有一个最初使用基于文件的会话编写的 CherryPy Web 应用程序。有时我会在会话中存储潜在的大型对象,例如运行报告的结果 - 我提供了以各种格式下载报告结果的选项,并且我不想在运行时重新运行查询用户选择下载是因为可能会获得不同的数据。在使用基于文件的会话时,效果很好。

现在我正在研究使第二台服务器联机的可能性,因此我需要能够在服务器之间共享会话数据,对于这种情况,使用 memchached 会话存储类型似乎是最合适的。我简要地研究了使用 PostgreSQL 存储类型,但是这个选项的文档记录很差,而且据我所知,很可能被破坏了。所以我实现了 memcached 选项。

但是,现在我遇到了一个问题,当我尝试将某些对象保存到会话时,我收到“AssertionError:id xxx 的会话数据未设置”。我假设这是由于对象大小超过了 CherryPy 会话后端或 memcached 中设置的任意限制,但我真的不知道,因为异常并没有告诉我为什么没有设置它。我已将 memcached 中的对象大小限制增加到最大 128MB 以查看是否有帮助,但它没有 - 无论如何这可能不是一个安全的选择。

那么我的解决方案是什么?有什么方法可以使用 memcached 会话存储来存储任意大的对象?我是否需要为这些对象“滚动我自己的”基于数据库或类似解决方案?问题可能不是基于大小的吗?还是我缺少其他选择?

【问题讨论】:

    标签: python cherrypy


    【解决方案1】:

    我使用mysql 来处理我的cherrypy 会话。只要对象是可序列化的(可以腌制),您就可以将其存储为 mysql 中的 blob(二进制大对象)。这是您要用于 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)
    

    希望这会有所帮助!

    【讨论】:

    • 您的代码链接不起作用,但从我在这里看到的情况来看,这将非常适合我的目的,只需稍作调整即可使其与 PostgreSQL 而不是 MySQL 一起使用。谢谢!
    【解决方案2】:

    听起来您想存储对存储在 Memcache 中的对象的引用,然后在需要时将其拉回,而不是依靠状态来处理加载/保存。

    【讨论】:

    • 好的,这听起来像是一个好的开始,但留下了以下问题:a)我如何/在哪里存储对象,以便任一服务器都可以访问它(也许是 db?)和 b)我如何处理获取会话到期时摆脱对象?
    【解决方案3】:

    根据您的解释,我可以得出结论,从概念上讲,混合用户会话和缓存并不是一个好主意。会话的主要设计目的是保持用户身份的状态。因此它具有安全措施、锁定、避免并发更改等方面。此外,会话存储通常是易失的。因此,如果您打算将会话用作缓存,您应该了解会话的实际工作方式及其后果。

    我建议您这样做以建立您的域模型的正常缓存,以生成报告数据并保持身份会话。

    CherryPy 详细信息

    默认 CherryPy 会话实现锁定会话数据。在 OLAP 情况下,您的用户可能无法执行并发请求(例如,打开另一个选项卡),直到报告完成。但是,可以选择手动锁定管理。

    PostgreSQL 会话存储已损坏,可能会在下一个版本中删除。

    Memcached 会话存储不实现分布式锁定,因此请确保使用一致的规则来平衡服务器之间的用户。

    【讨论】:

    • 使用一般“缓存”而不是会话数据的问题在于,报告结果是 a) 特定于用户的,并且 b) 是临时的。使用 session 对象自动为我提供了这两个功能。使用其他东西,我必须自己实现它们 - 如果这是最好的做事方式,那很好,但如果不是,则需要做很多额外的工作。
    • @ibrewster 满足您的要求 a) 将 userId 添加到密钥 b) 为缓存条目设置 TTL。只要您了解其语义,就可以对案例使用会话存储。是的,它是自动出现的,但它不是免费的。我提供了详细信息。顺便说一句,许多 Python 缓存库的缓存就像在您的方法中应用装饰器一样简单。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-26
    相关资源
    最近更新 更多