问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

PHP中SESSION的问题

发布网友 发布时间:2022-04-07 05:24

我来回答

4个回答

懂视网 时间:2022-04-07 09:45

一个真实案例

想起自己之前犯过一个相关的错误。当时场景是这样的:有一个微信公众号项目,调用微信公众号的接口都需要access_token,它的有效期是2小时。当时我的做法是把它存放在文件中,格式使用的是json{"access_token":"easWasdw32323", "expire":1588219064}。伪代码如下:

function getToken ($tokenFile)
{
 $tokenJson = file_get_contents($tokenFile);
 
 if (!$tokenJson) {
 $token = loadToken($tokenFile);
 } else if (json_decode($tokenJson, true)['expire'] <= time()){
 $token = loadToken($tokenFile);
 } else {
 $token = json_decode($tokenJson, true)['access_token'];
 }
 
 return $token;
}
function loadToken ($tokenFile) 
{
 $fp = fopen($tokenFile, 'r+');
 
 $tokenJson = ...; // 调用微信接口获取到token
 fwrite($fp, json_encode($tokenJson));
 
 return $tokenJson['access_token'];
}

出现的问题:

项目运行一段时间后就会出问题,但过一两秒后再刷新就正常了。

问题原因分析:

假设token已经过期了,这时候有2个请求来了,分别命名为A、B。A来了,发现token到期后,去调用微信接口获取新的token,获取后,更新到存放token的文件中。

但是,文件没有完全更新完毕的时候,B来了,读入存放token的文件。因为token文件中数据没有更新完整,B读到的数据就会产生错误。

另外还有可能是A和B同时在更新文件内容,这样就会产生数据混乱,也会导致错误发生。

如何规避这个错误呢?

文件锁机制可以完成。

在PHP中提供了 flock()函数,可以对文件使用锁定机制(锁定或释放文件)。当一个进程在访问文件时加上锁,其他进程要想对该文件进行访问,则必须等到锁定被释放以后。这样就可以避免在并发访问同一个文件时破坏数据。

函数原型如下:

flock ( resource $handle , int $operation [, int &$wouldblock ] ) : bool

  • handle

    文件系统指针,是典型地由 fopen() 创建的 resource(资源)。

  • operation

  • operation 可以是以下值之一:

    LOCK_SH取得共享锁定(读取的程序)。

    LOCK_EX 取得独占锁定(写入的程序)。

    LOCK_UN 释放锁定(无论共享或独占)。

    LOCK_NB附加锁定(Windows 上还不支持)。

  • wouldblock

  • 如果锁定会堵塞的话(EWOULDBLOCK 错误码情况下),可选的第三个参数会被设置为 TRUE。(Windows 上不支持)

    demo

    demo1.php

    <?php
     
    $file = 'data.txt';
    $handler = fopen($file, 'a+') or die('文件资源打开失败');
    // 取得独占锁
    if (flock($handler, LOCK_EX)) {
     sleep(5);
     flock($handler, LOCK_UN);
    } else {
     echo '锁定失败';
    }
     
    fclose($handler);

    demo2.php

    <?php
    $file = 'data.txt';
    $handler = fopen($file, 'a+') or die('文件资源打开失败');
     
    // 取得独占锁
    if (flock($handler, LOCK_EX)) {
     fwrite($handler, 'sometest string');
     flock($handler, LOCK_UN);
    } else {
     echo '锁定失败';
    }
     
    fclose($handler);

    先运行demo1.php然后立即运行demo2.php,会发现,因为被demo1.php锁定了文件,demo2.php写入不了新内容,只有等demo1.php释放了锁定,demo2.php才能拿到独占锁,然后才能写入文件。

    解决问题

    学完这些知识后,就能解决我之前的问题了。改进的代码如下:

    <?php
    function getToken ($tokenFile){
     $tokenJson = file_get_contents($tokenFile);
     
     if (!$tokenJson) {
      $token = loadToken($tokenFile); 
     } else if (json_decode($tokenJson, true)['expire'] <= time()){ 
      $token = loadToken($tokenFile);
     } else {
      $token = json_decode($tokenJson, true)['access_token']; 
     }
     return $token;
    }
    
    function loadToken ($tokenFile) {
     $fp = fopen($tokenFile, 'w'); // 取得独占锁 
     if (flock($fp, LOCK_EX)) {
     $tokenJson = ...; // 调用微信接口获取到token 
     fwrite($fp, json_encode($tokenJson)); 
     flock($fp, LOCK_UN); 
     } else {
     return false; 
     }
     
     return $tokenJson['access_token'];
    }

    热心网友 时间:2022-04-07 06:53

      Session 和 Cookie 有什么关系
      Cookie 也是由于 HTTP 无状态的特点而产生的技术。也被用于保存访问者的身份标示和一些数据。每次客户端发起 HTTP 请求时,会将 Cookie 数据加到 HTTP header 中,提交给服务端。这样服务端就可以根据 Cookie 的内容知道访问者的信息了。 可以说,Session 和 Cookie 做着相似的事情,只是 Session 是将数据保存在服务端,通过客户端提交来的 session_id 来获取对应的数据;而 Cookie 是将数据保存在客户端,每次发起请求时将数据提交给服务端的。
      上面提到,session_id 可以通过 URL 或 cookie 来传递,由于 URL 的方式比 cookie 的方式更加不安全且使用不方便,所以一般是采用 cookie 来传递 session_id。
      服务端生成 session_id,通过 HTTP 报文发送给客户端(比如浏览器),客户端收到后按指示创建保存着 session_id 的 cookie。cookie 是以 key/value 形式保存的,看上去大概就这个样子的:PHPSESSID=e4tqo2ajfbqqia9prm8t83b1f2。在 PHP 中,保存 session_id 的 cookie 名称默认叫作 PHPSESSID,这个名称可以通过 php.ini 中 session.name 来修改,也可以通过函数 session_name() 来修改。
      为什么不推荐使用 PHP 自带的 files 型 Session 处理器
      在 PHP 中,默认的 Session 处理器是 files,处理器可以用户自己实现(参见:自定义会话管理器)。我知道的成熟的 Session 处理器还有很多:Redis、Memcached、MongoDB……
      为什么不推荐使用 PHP 自带的 files 类型处理器,PHP 官方手册中给出过这样一段 Note:
      无论是通过调用函数 session_start() 手动开启会话, 还是使用配置项 session.auto_start 自动开启会话, 对于基于文件的会话数据保存(PHP 的默认行为)而言, 在会话开始的时候都会给会话数据文件加锁, 直到 PHP 脚本执行完毕或者显式调用 session_write_close() 来保存会话数据。 在此期间,其他脚本不可以访问同一个会话数据文件。
      上述引用参见:Session 的基本用法
      为了证明这段话,我们创建一下 2 个文件: 文件:session1.php
      <?php
      session_start();
      sleep(5);
      var_mp($_SESSION);
      ?>
      文件:session2.php
      <?php
      session_start();
      var_mp($_SESSION);
      ?>
      在同一个浏览器中,先访问 http://127.0.0.1/session1.php,然后在当前浏览器新的标签页立刻访问 http://127.0.0.1/session2.php。实验发现,session1.php 等了 5 秒钟才有输出,而 session2.php 也等到了将近 5 秒才有输出。而单独访问 session2.php 是秒开的。在一个浏览器中访问 session1.php,然后立刻在另外一个浏览器中访问 session2.php。结果是 session1.php 等待 5 秒钟有输出,而 session2.php 是秒开的。
      分析一下造成这个现象的原因:上面例子中,默认使用 Cookie 来传递 session_id,而且 Cookie 的作用域是相同。这样,在同一个浏览器中访问这 2 个地址,提交给服务器的 session_id 就是相同的(这样才能标记访问者,这是我们期望的效果)。当访问 session1.php 时,PHP 根据提交的 session_id,在服务器保存 Session 文件的路径(默认为 /tmp,通过 php.ini 中的 session.save_path 或者函数 session_save_path() 来修改)中找到了对应的 Session 文件,并对其加锁。如果不显式调用 session_write_close(),那么直到当前 PHP 脚本执行完毕才会释放文件锁。如果在脚本中有比较耗时的操作(比如例子中的 sleep(5)),那么另一个持有相同 session_id 的请求由于文件被锁,所以只能*等待,于是就发生了请求阻塞的情况。
      既然如此,在使用完 Session 后,立刻显示调用 session_write_close() 是不是就解决问题了哩?比如上面例子中,在 sleep(5) 前面调用 session_write_close()。
      确实,这样 session2.php 就不会被 session1.php 所阻塞。但是,显示调用了 session_write_close() 就意味着将数据写到文件中并结束当前会话。那么,在后面代码中要使用 Session 时,必须重新调用 session_start()。
      例如:
      <?php
      session_start();
      $_SESSION['name'] = 'Jing';
      var_mp($_SESSION);
      session_write_close();
      sleep(5);
      session_start();
      $_SESSION['name'] = 'Mr.Jing';
      var_mp($_SESSION);
      ?>
      官方给出的方案:
      对于大量使用 Ajax 或者并发请求的网站而言,这可能是一个严重的问题。 解决这个问题最简单的做法是如果修改了会话中的变量, 那么应该尽快调用 session_write_close() 来保存会话数据并释放文件锁。 还有一种选择就是使用支持并发操作的会话保存管理器来替代文件会话保存管理器。
      我推荐的方式是使用 Redis 作为 Session 的处理器。
      拓展阅读:
      为什么不能用 memcached 存储 Session
      如何使用 Redis 作为 PHP Session handler
      Session 数据是什么时候被删除的
      这是一道经常被面试官问起的问题。
      先看看官方手册中的说明:
      session.gc_maxlifetime 指定过了多少秒之后数据就会被视为"垃圾"并被清除。 垃圾搜集可能会在 session 启动的时候开始( 取决于 session.gc_probability 和 session.gc_divisor)。 session.gc_probability 与 session.gc_divisor 合起来用来管理 gc(garbage collection 垃圾回收)进程启动的概率。此概率用 gc_probability/gc_divisor 计算得来。例如 1/100 意味着在每个请求中有 1% 的概率启动 gc 进程。session.gc_probability 默认为 1,session.gc_divisor 默认为 100。
      继续用我上面那个不太恰当的比方吧:如果我们把物品放在超市的储物箱中而不取走,过了很久(比如一个月),那么保安就要清理这些储物箱中的物品了。当然并不是超过期限了保安就一定会来清理,也许他懒,又或者他压根就没有想起来这件事情。
      再看看两段手册的引用:
      如果使用默认的基于文件的会话处理器,则文件系统必须保持跟踪访问时间(atime)。Windows FAT 文件系统不行,因此如果必须使用 FAT 文件系统或者其他不能跟踪 atime 的文件系统,那就不得不想别的办法来处理会话数据的垃圾回收。自 PHP 4.2.3 起用 mtime(修改时间)来代替了 atime。因此对于不能跟踪 atime 的文件系统也没问题了。
      GC 的运行时机并不是精准的,带有一定的或然性,所以这个设置项并不能确保旧的会话数据被删除。某些会话存储处理模块不使用此设置项。
      对于这种删除机制,我是存疑的。
      比如 gc_probability/gc_divisor 设置得比较大,或者网站的请求量比较大,那么 GC 进程启动就会比较频繁。
      还有,GC 进程启动后都需要遍历 Session 文件列表,对比文件的修改时间和服务端的当前时间,判断文件是否过期而决定是否删除文件。
      这也是我觉得不应该使用 PHP 自带的 files 型 Session 处理器的原因。而 Redis 或 Memcached 天生就支持 key/value 过期机制的,用于作为会话处理器很合适。或者自己实现一个基于文件的处理器,当根据 session_id 获取对应的单个 Session 文件时判断文件是否过期。
      为什么重启浏览器后 Session 数据就取不到了
      session.cookie_lifetime 以秒数指定了发送到浏览器的 cookie 的生命周期。值为 0 表示"直到关闭浏览器"。默认为 0。
      其实,并不是 Session 数据被删除(也有可能是,概率比较小,参见上一节)。只是关闭浏览器时,保存 session_id 的 Cookie 没有了。也就是你弄丢了打开超市储物箱的钥匙(session_id)。
      同理,浏览器 Cookie 被手动清除或者其他软件清除也会造成这个结果。
      为什么浏览器开着,我很久没有操作就被登出了
      这个是称为“防呆”,为了保护用户账户安全的。
      这个小节放进来,是因为这个功能的实现可能和 Session 的删除机制有关(之所以说是可能,是因为这个功能不一定要借住 Session 实现,用 Cookie 也同样可以实现)。 说简单一点,就是长时间没有操作,服务端的 Session 文件过期被删除了。
      一个有意思的事情
      在我试验的过程中,发现了小有意思的事情:我把 GC 启动的概率设置为 100%。如果只有一个访问者请求,该访问者即使过了很久(超过了过期时间)后才发起第二次请求,那么 Session 数据也还是存在的('session.save_path' 目录下面的 Session 文件存在)。是的,明明就超过了过期时间,却没有被 GC 删除。这时,我用另外一个浏览器访问时(相对于另一个访问者),这次请求生成了新的 Session 文件,而上一个浏览器请求生成的那个 Session 文件终于没有了(之前那个 Session 文件在 'session.save_path' 目录下面的消失了)。
      还有,发现 Session 文件被删除后,再次请求,还是会生成和之前文件名相同的 Session 文件(因为浏览器并没有关闭,再次请求发送的 session_id 是相同的,所以重新生成的 Session 文件的文件名还是一样的)。但是,我不理解的是:这个重新出现的文件的创建时间竟然是第一次的那个创建时间,难道它是从回收站中回来的?(确实,我做这个试验时是在 window 下进行的)
      我猜测的原因是这样:当启动会话后,PHP 根据 session_id 找到并打开了对应的 Session 文件,然后才启动 GC 进程。GC 进程就只检查除了当前这个 Session 文件外的其他文件,发现过期的就干掉。所有,即使当前这个 Session 文件已经过期了,GC 也没有删除它。
      我认为这个不合理的。
      由于发生这种情况影响也不大(毕竟线上请求很多,当前请求的过期文件被其他请求唤起的 GC 干掉的可能性是比较大的) + 我没有信心去看 PHP 源代码 + 我并不在线上使用 PHP 自带的 files 型 Session 处理器。所以,这个问题我就没有深入研究了。请谅解。
      <?php
      // 过期时间设置为 30 秒
      ini_set('session.gc_maxlifetime', '30');
      // GC 启动概率设置为 100%
      ini_set('session.gc_probability', '100');
      ini_set('session.gc_divisor', '100');
      session_start();
      $_SESSION['name'] = 'Jing';
      var_mp($_SESSION);
      ?>

    热心网友 时间:2022-04-07 08:11

    $_SESSION["$string"],$string不能是纯数字

    热心网友 时间:2022-04-07 09:46

    $_SESSION["$booknum"][3]
    声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
    whether...ornot必须搭配着用么? whether必须跟or not吗 z97主板与b85有什么区别? b85主板和z97的区别有哪些 z97和b85有什么区别? shuxpai锁了屏怎么办? AI软件在保存文件时,出现"0x01d93ff0"指令引用的“0x00000000”内存。该... AI文件保存时保存不了。无法以此文件名存储。请以其它名称或在其它文... 上海买衣服最实惠的地方再哪 在上海买衣服去哪好呢? 物业管理的工作范围有哪些 保姆的工作范围是什么 梦见一个人走在泥泞的小路上 消防员工作的工作范围? 周公解梦梦见自己和一群人在泥泞中走,鞋裤腿都是泥 社会工作者的工作范围? 梦见我与几个人一起走在泥泞的下坡路上,我轻轻的过去了,其他人没有过去... 怎么取消QQ可能认识的人推荐,不是推荐给我,是不想把我的好友推荐给别人... 如何关掉qq推荐好友 以“大力植树造林,应对气候变化,建设生态文明”为题,一篇800字文章。可以了,追加分 有关大力植树造林应对气候变化建设生态文明的作文600字以上 急!!!!!!!!!!! 大人植树的作文30字 大力植树造林,应对气候变化,创建文明生态,800~2000字作文 怎样设计调查报告方案? 植树节到了,某市举行大型植树活动,共有1430人参加植树,要把人数分成相等的若干队,且每队人数在100至2 大力植树造林可以改善气候的原因是什么 请求解答==国家边疆大植树,绿化造林为儿孙==猜猜哪一种动物 大和小一起植树,两人共植树24棵,大多植2棵,小少植2棵,那么两人植树棵树相同,大和小分别植多少棵 怎么设计调查方案? 国家边疆大植树,绿化造林为儿孙解什么意思,猜什么生肖,怎么解释 新浪微博能不能看到访客记录?会员呢?能不能看到? 保密管理办法 本人刚自学通风空调工程,请问风机盘管和风口有什么不同??我看它们的外形差不多 1。机关,单位应当实行( ),健全保密管理制度,完善保密防护措施,开展保密宣传教育` 2。( )方面的国 这个图哪个是风盘的回风口和送风口? 一台风机盘管可以设几个出风口?02风机盘管开多大出风口 怎么通过风机盘管的尺寸来确定风机盘管风口尺寸 是不是每个风机盘管都有一个送风口和一个回风口? 16年前住过院,2年前买了好医保没有告知,现再次住院能理赔吗? 风机盘管可以这样连接四个风口么?和风机盘管连接的尺寸怎么确定?急急急~ 包皮手术后的图片 风机盘管接风管做下送风形式,风管与风口之间用铁皮硬接还是用软接? 有这方面的规范要求吗? 好医保理赔一次就没了吗 有谁知道各种型号风机盘管的风口尺寸啊?? 包皮过长怎么办?手术后外观会有什么不同?不手术能不能预防? 我帮我妈在平安买了份好医保但十年前他住过院什么病我忘记了会不会被拒赔呢? 包皮过长术后的注意事项有哪些 风机盘管的回风口在哪里呀,我看了半天我也没找到风机盘管本身的回风口在哪里? 包皮过长手术后该注意哪些? 有人用好医保赔付过吗