PHP Redis会话无法保存

6

编辑 我尝试使用xdebug和netbeans进行调试。奇怪的是,如果我加入一些断点,导出操作将在调试会话期间起作用。但是,在没有断点的情况下,更真实的环境下,导出操作不起作用。

我尝试在代码的某些部分添加等待时间。

我认为可能是PHP在Redis提交完成之前就结束了。也许Redis连接是异步完成的,但我检查了PRedis,它的默认连接是同步的。


我正在开发一个报告工具。

以下是基本问题。

我们将报告存储到会话对象中,但在以后的请求中,当我们尝试获取会话对象中的报告时,它已经消失了。

以下是更详细的版本。

我像这样将“报告”对象存储到会话中:

  $_SESSION['report_name_unixtimestamp'] = gzcompress( serialize( $reportObject ) );

用户以表格形式查看报告,如果需要,可以导出报告。报告可能会更改,因此将其存储在会话中的想法是,当用户将其导出到PDF、Excel等格式时,他们将获得与正在查看的报告完全相同的报告。
用户单击导出按钮后,在PHP端,它将进入会话,通过提供的键作为GET参数获取报告(解压缩和反序列化它),创建导出并将其发送给用户进行下载。
这一直运作良好,直到我们尝试将Redis缓存服务器引入作为更好的会话管理工具。
现在发生的情况如下:
第一次运行报告时,它将被存储到缓存中,并且导出将成功工作。
我们将再次运行报告,使用相同的用户帐户在同一个会话中。这将更改unixtimestamp,因此应该有两个条目在 $_SESSION 中。($_SESSION ['report_name_oldertimetamp'] $_SESSION ['report_name_newertimestamp'] )。当我们再次单击导出按钮时,我们会收到一个错误,指示文件不存在(因为服务器尚未发送它)。
如果我们检查redis服务器以获取报告的更新版本,则不会找到它,但旧的时间戳仍然存在。
现在,这在文件会话管理中可以正常工作,但在Redis中不行。我们已经尝试了PHP的Redis模块以及纯PHP客户端Predis。
有人有什么想法吗?
以下是使用Predis的保存处理程序。
redis_session_init是我在session_start()之前调用的函数,以便注册它。但我不确定redis_session_write函数如何工作,所以也许有人可以帮助我。
    <?php
    namespace RedisSession
    {

        $redisTargetPrefix = "PHPREDIS_SESSION:";
        $unpackItems = array( );
        $redisServer = "tcp://cache.emcweb.com";

        function redis_session_init( $unpack = null, $server = null, $prefix = null )
        {
            global $unpackItems, $redisServer, $redisTargetPrefix;

            if( $unpack !== null )
            {
                $unpackItems = $unpack;
            }

            if( $server !== null )
            {
                $redisServer = $server;
            }

            if( $prefix !== null )
            {
                $redisTargetPrefix = $prefix;
            }

            session_set_save_handler( 'RedisSession\redis_session_open', 'RedisSession\redis_session_close', 'RedisSession\redis_session_read', 'RedisSession\redis_session_write', 'RedisSession\redis_session_destroy', 'RedisSession\redis_session_gc' );
        }

        function redis_session_read( $id )
        {
            global $redisServer, $redisTargetPrefix;

            $redisConnection = new \Predis\Client( $redisServer );
            return base64_decode( $redisConnection->get( $redisTargetPrefix . $id ) );
        }

        function redis_session_write( $id, $data )
        {
            global $unpackItems, $redisServer, $redisTargetPrefix;

            $redisConnection = new \Predis\Client( $redisServer );
            $ttl = ini_get( "session.gc_maxlifetime" );

            $redisConnection->pipeline( function ($r) use (&$id, &$data, &$redisTargetPrefix, &$ttl, &$unpackItems)
        {
            $r->setex( $redisTargetPrefix . $id, $ttl, base64_encode( $data ) );

            foreach( $unpackItems as $item )
            {
                $keyname = $redisTargetPrefix . $id . ":" . $item;

                if( isset( $_SESSION[ $item ] ) )
                {
                    $r->setex( $keyname, $ttl, $_SESSION[ $item ] );
                }
                else
                {
                    $r->del( $keyname );
                }
            }
        } );
        }

        function redis_session_destroy( $id )
        {
            global $redisServer, $redisTargetPrefix;

            $redisConnection = new \Predis\Client( $redisServer );
            $redisConnection->del( $redisTargetPrefix . $id );

            $unpacked = $redisConnection->keys( $redisTargetPrefix . $id . ":*" );

            foreach( $unpacked as $unp )
            {
                $redisConnection->del( $unp );
            }
        }

        // These functions are all noops for various reasons... opening has no practical meaning in
        // terms of non-shared Redis connections, the same for closing. Garbage collection is handled by
        // Redis anyway.
        function redis_session_open( $path, $name )
        {

        }

        function redis_session_close()
        {

        }

        function redis_session_gc( $age )
        {



        }
    }

你可以 (仅为诊断目的) 使用不同的键来设置会话变量吗?例如 md5('report_name_unixtimestamp') 或者 'timestamp-reportname' ?背景:我怀疑键被截断了。 - Eugen Rieck
@EugenRieck,你的意思是像这样 $_SESSION[md5('report_name_1234')] 吗?也许是这样,但这并不能解释为什么第一个报告导出可以正常工作。报告的密钥长度相同,因为它们具有相同的名称,只是时间戳不同。我会尝试一下,谢谢。 - Jerry Saravia
@EugenRieck,不行。我尝试按照你建议的做MD5哈希,但它也不起作用。 - Jerry Saravia
我的意思是:假设密钥在读取路径上被截断为“report_name”,然后客户端执行GETKEYS,随后是GET。使用 $_SESSION[md5('report_name_1234')] 可以帮助我们排除这种可能性。 - Eugen Rieck
谢谢和抱歉,我已经没有更多的想法了。 - Eugen Rieck
我尝试使用一些更简单的代码来复制这个问题。在我的示例中,所有内容都正常工作。这让我相信实际应用程序中的某些代码导致它无法写入会话。 - Jerry Saravia
1个回答

4
问题已经得到解决,解决方案比我想象的简单得多。
保存处理程序没有以任何方式实现锁定。在报告页面上,通过ajax等方式向服务器发送多个请求。其中一个ajax请求在报告被保存到会话空间之前开始。因此,它读取会话,然后在末尾写入该会话。
由于报告每次都会更快地执行,因此报告会被缓存到Redis会话中,但随后会被具有较旧版本会话的其他脚本覆盖。
我得到了一位同事的帮助。唉!这是一个我很高兴结束的头痛。

我们最终决定不使用Redis进行会话管理,而是暂时坚持使用PHP会话。但解决方案是编写一个保存处理程序锁定机制,以便不同请求捕获对会话的更改。 - Jerry Saravia
我其实考虑过这个问题。不是真正的锁定,而是在每个会话上维护一个校验和,当你写入它时,如果校验和已更改,则需要合并两个数组,否则,只需覆盖。在PHP中很容易做到,但在CPP中不太容易。 - Itay Moav -Malimovka
是的。@ItayMoav-Malimovka,我可能会使用时间戳,但这只是我的观点。如果保存时的时间戳与读取时的时间戳不同,则将新会话与您的会话合并。如果时间戳相同,则写入会话并更新时间戳。典型的乐观锁定。 - Jerry Saravia
为什么你不直接使用 PHP 内置的会话处理程序并将其指向 Redis 呢?由于 PHP 本身支持文件或 memcache 作为存储,我不明白为什么你不能使用它?Redis 是兼容 memcache 的。 - Daniele Testa
1
根据我的经验,我也建议不要使用Redis。它容易出错。 - Mickey Pearson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接