原文地址:https://t.ti-node.com/thread/…
这基本上算是个老旧的话题了,几乎所有 phper 在第一次面试的时候都会被问到关于 session 的问题,如果不出意外,往往是如下三板斧:
php 的 session 是什么东西
php 的 session 存在什么地方、时候过期
php 的 session 和 cookie 有什么区别
这三个问题堪称是关于 php session 三大基础问题了,要是掌握不好,直接导致面试挂掉,令人唏嘘不已。
就以上三个问题简单回顾一下:
session 翻译成中文,大抵就是会话。我们知道 http 协议是一个无状态的协议,用极为粗暴的土话表示无状态就是说“你压根不知道这个 http 请求是哪个犊子发起的”或者“你永远不知道这个 http 请求的那头到底是上次那个哈士奇还是上上次那个胖子”,所以为了解决这个问题,php 引入 session 来额外标记“到底谁发起的这个 http 请求”。在 php 中,php 会为每个不同的用户生成一个随机的 session id,每个人拥有的 session id 都是不同的。用户在与服务器产生的每一次交互中,都是利用 session id 来辨别的用户。让 php 产生 session 是一件很容易的事情,直接调用 session_start() 函数就可以了,如下图就是产生的 session 文件:
其中 sess 是文件前缀,后面的“njjf8l3lh**ff6”就是你的 session id 了。但是现在 session 文件内容是空的,如果我们用如下代码,就可以产生 session 文件的内容:
<?php
// 开启 session
session_start();
$_SESSION[‘hello’] = ‘world’;
刷新一下网页,然后在再次查看原来的 session 文件,其中就会有如下内容:就是说,你往全局变量 $_SESSION 保存的内容本质上都是 PHP 用文本形式给你存储到服务器上了。服务器根据你的 session id 读取相应的 session 文件然后把其中内容读出来,你就会得到你的 $_SESSION 数据。
php 的 session 默认是以文件的形式存储的系统磁盘中,在运行于 ubuntu 16.04 系统的 php 7.0.28 中,session 是存储于 /var/lib/php/sessions 文件夹下,可以通过 php.ini 配置文件中的 session.save_path 来查看确定。php 的过期时间是由 php 配置项 session.gc_maxlifetime 来确定的,值的单位是秒钟,默认是 1440,也就是说当这个 session 文件具体上次修改时间超过了 1440 秒后这个 session 文件就算是过期了。值得注意的是,过期了不代表这个 session 文件会马上被垃圾回收机制删除掉,还是有可能会残存一段时间的。那么,究竟何时会被删除?这取决于 session.gc_probability 和 session.gc_divisor 两个配置项了。这两个选项的比值 (session.gc_probability / session.gc_divisor) 就是触发垃圾回收机制的概率,比如 (1 / 100) 就可以简单粗暴的理解为“每产生 100 个请求,就有 1 次会触发 php 垃圾回收机制去删除过期的 session 文件”,所以你记住了:在 php 中如果你想要一个精确过期的 session 文件,最起码默认的 session 配置是绝对不可能的。话说回来,还不都是因为 php 并没有启动一个单独的线程或者进程去扫描垃圾,所以,也只能用这个“概率”这种粗暴的方式来解决这个问题,又不是不能用。
开篇说了,为了搞明白“到底是哈士奇还是胖子”的问题,不得不引入额外标记数据才行,所以实际上,先有的 cookie 而有后的 session,都是为了解决这个问题而产生的。二者的恩怨情仇在于:
cookie 存在于客户端,而 session 存在于服务器端,所以 session 相对更安全
服务器可以读取 session 和 cookie,但是客户端(也就是浏览器)只能读取 cookie
默认情况下,session 是离不开 cookie 的,说到底“安全的 session 使用方式”必须依靠 cookie 才能混口饭吃。因为 session id 就是依靠 cookie 保存起来的,客户端浏览器每次发送请求都会携带上该 cookie,该 cookie 默认名称是 PHPSESSID,数值就是 session id。
如果说就是用不了 cookie,那么 session 也并不是真的不能用。禁用 cookie 的情况下,session 可以通过配置利用 URL 来传递,也就是 query string 直接暴露在网址中,非常不安全非常吓人,严重不推荐这种方式,甚至应该直接禁用!
cookie 大小稍微有限制(据说考虑用 localstorage 代替?),session 相对宽松
大概就这些,不再赘述,我是建议大家配合 php.ini 文件去研究上面三个问题。
如果说真的只回顾一下这三个问题,那岂不是真的应了“一看标题猛如虎,打开内容 1 -5”?我说过了的,我这里是个正经的博客网站,是个真正的有些内涵的 php 文化网站,不能只讲些个初级的内容,是个话题都都要无论如何强塞点儿看起来高端的玩意进去撑场面。
刚叨叨过了,默认配置下 session 是以文本文件形保存在服务器的某个文件夹中的,有心的人应该知道“一个目录中文件过多是会降低读取效率的”,所以,在用一些 PC 软件的时候可以看到这些软件会把 TA 需要的数据分散开来到不同的次级目录中去。php 的 session 文件也可以这么干,总体来说是比较简单粗暴的。我们需要关注下两个 php 配置项:
一个是 session.save_handler,默认这货的值是“files”,也就是文件
一个是 session.save_path,默认这货的值是一个目录路径,比如 /var/lib/php/session。现在我们将这个值改成类似于 session.save_path = “N;/path” 这样的,其中 N 是一个正整数,这个数值的含义就是指将目录分成几个层次,比如我们修改成 session.save_path =“2;/var/lib/php/sessions”,然后重启一下 apache 或者 fpm 进程管理器,然后执行如下代码:
<?php
echo “let rock session”;
session_start();
刷新网页,如下图所示:错误原因相比大家看到了,大概意思就是说“/var/lib/php/sessions/n/j/”这个文件夹不存在,那么切换到这个目录下看看,如下图:果然是空的,也就说没有 /n/ j 这个子目录,看来得手工创建了。然而,真的不能去手工创建,因为你哪儿知道文件夹的名字是啥?回到配置文件一顿研究,在 session.save_path 配置项附近发现如下英文字样:
; NOTE 1: PHP will not create this directory structure automatically.
; You can use the script in the ext/session dir for that purpose.
; NOTE 2: See the section on garbage collection below if you choose to
; use subdirectories for session storage
英文比较蹩脚昂,大概翻译一下,多包涵:
; NOTE 1: PHP 压根不会帮你创建这些文件夹,您自己个儿下载 php 源码包,到 ext 目录的 session 目中去找那个脚本去创建
; NOTE 2: 如果你要用子目录存储 session 的话,记得看下垃圾回收,不看就有坑。(坑在这里直接告诉大家吧,大概就是说你要自己搞子目录存 session,那我那个靠信仰和概率才能触发的垃圾回收机制就压根就不触发了,你自己想办法搞定你的过期 session,我不管了)
所以呢,我们下载一个 php 源码包,最好是和你运行环境版本一样的 php 源码包并解压,命令行切到 ext/session 目录下,如下图:看到那个 mod_files.sh 没?Linux 下就这脚本。mod_files.bat 就是给 windows 用的。给这个脚本 chmod +x mod_files.sh 加个执行权限,然后查看下使用方式:为了帮助眼近视的读者,友情翻译一下使用方式:
./mod_files.sh ‘session 文件根目录 ’ 目录深度 哈希函数比特量
对应我的 php 开发环境就是:
./mod_files.sh /var/lib/php/sessions/ 2 5
其中第一项就是你存储 session 的根目录,第二项就是那个 N,第三项查看 session.hash_bits_per_character 配置项
然后执行,如下图所示:此时到 /var/lib/php/sessions 中查看下,果然有目录了,那么,再次刷新网页,本以为很顺利的你可能依然会遇到错误,如下:
session_start(): open(/var/lib/php/sessions/n/j/sess_njjf8l3lhfrpq8nrlnl1d9qff6, O_RDWR) failed: Permission denied (13)
模模糊糊认得 Permission denied 这几个字母,好像是权限的问题,难道是因为当前 apache 进程用户或者 fpm 进程用户没有权限往这些目录写数据吗?改下这些目录的拥有者撒,改成 www-data(我系统中 fpm 的运行用户),再试试,果然好了!
总有刁民以为这就可以解决很大的问题了,然而很悲剧的是:并不是。当前这个方案一定程度可以解决 session 文件过多的问题,但是依然有两个问题没有得到解决:
依然是文件存储,如果访问量太大的话,session 文件从硬盘的读取 IO 或许会成为程序的瓶颈,当然 SSD 速度一定会好很多
如果网站分别部署到了两台服务器上,session 无法共享,出现故障。什么意思呢?就是为了保证高可用,网站程序分别在 A 服务器上和 B 服务器上,然后最外面使用一台 nginx 挡在最前面做负载均衡,路人甲的某次 http 请求可能会被分配到 A 上,也可能会被分配到 B 上。路人甲在 A 上产生的 session 文件会被保存到 A 服务器硬盘上,但是服务器 B 上却没有,如果该用户请求被打到 B 上的时候,很不幸,session 丢失了,一些数据也就会丢失,路人甲八成会骂娘骂客服。也就说,A 服务器和 B 服务器需要共享同一套 session!
借此,就引入一个问题,就是分布式 web 部署中,如何解决 session 共享的问题!
关子我就不卖了,没意思,首先想到的是 redis,为 A 和 B 提供一台 C redis 服务器就可以了,这样可以“多快好省”地一举解决问题!按照预想,引入 redis 后可以顺利解决三个问题:
内存级的读写速度,唰唰唰!
session 轻松 easy 实现了共享,哪怕以后业务服务器继续横向扩展到服务器 D
session 的过期终于可以精确到秒了,说没就没,不用再靠信仰和概率了
将 session 存入 redis 需要修正如下两处 php 配置,首先设置 session.save_handler = redis,其次是设置 ession.save_path = “tcp://127.0.0.1:6379″,然后重启 apache 或者 fpm,刷新一下网页,如果网页不报什么错误,理论上 session 数据就已经到 redis 中去了,连接 redis 查看下 key,如下图:
从上至下我一共执行了五次 redis 命令,分别表示:
查看所有 keys 从而获取 php 分配给我的 session 文件名称
获取这个 key 的剩余时间,过期后 redis 该 key 算失效了
查看这个 key 的数据类型,可以看出是 string 类型
使用 get 直接获取 string 的值
过了一段时间,我又刷新了一下网页,然后再次用 ttl 看该 key 的剩余时间,再次被延长到 1440 秒了
除了 redis 外(memcache 我就不举例了,和 redis 类似),还有一种方案就是通过 nfs 共享来实现,大概原理就是弄一台服务器,通过内网共享对所有 php 业务服务器开放读写,大家知道 linux 下磁盘是可以挂载在某个文件夹下的,所以将 nfs 挂载到各个 php 业务服务器的某个目录下,然后按照上述文章修改响应配置就可以了。这个,我也没有尝试过也懒得自己去尝试了,所以偷个懒直接给大家抛个连接吧,是老叶博客上的一篇文章,《iMySQL | 老叶茶馆,PHP 实现多服务器 session 共享之 NFS 共享》。
装逼完毕,如有问题,火速留言指正!