好的。答案会更长一些 - 耐心等待!
1)无论我要写什么,都是基于我过去几天所做的实验。可能有一些我可能不知道的旋钮/设置/内部工作。如果您发现错误/或不同意,请大声疾呼!
2) 第一个说明 - 读取和写入会话数据时
即使您的脚本中有多次 $_SESSION 读取,会话数据也只会被读取一次。从会话中读取是基于每个脚本的。此外,数据获取是基于 session_id 而不是键发生的。
2) 第二个说明 - 写在脚本末尾总是调用
A) 对会话 save_set_handler 的写入总是被触发,即使对于只从会话中“读取”并且从不进行任何写入的脚本也是如此。
B) 写入只触发一次,在脚本结束时或者如果您明确调用 session_write_close。同样,写入是基于 session_id 而不是键
3) 第三个说明:为什么我们需要锁定
- 这是怎么回事?
- 我们真的需要锁定会话吗?
- 我们真的需要一个大锁包装 READ + WRITE
解释大惊小怪
脚本1
- 1: $x = S_SESSION["X"];
- 2:睡眠(20);
- 3: if($x == 1) {
- 4: //做点什么
- 5: $_SESSION["X"] = 3 ;
- 6:}
- 4:退出;
脚本 2
- 1: $x = $_SESSION["X"];
- 2: if($x == 1) { $_SESSION["X"] = 2 ; }
- 3:退出;
不一致之处在于脚本 1 正在执行基于会话变量 (line:3) 值的操作,该值已在脚本 1 已经运行时被另一个脚本更改。这是一个框架示例,但它说明了这一点。事实上,您正在根据不再正确的事物做出决定。
当您使用 PHP 默认会话锁定(请求级别锁定)时,脚本 2 将在第 1 行阻塞,因为它无法从脚本 1 从第 1 行开始读取的文件中读取。所以对会话数据的请求是序列化的。 script2读取一个值时,保证读取的是新值。
说明 4:PHP 会话同步不同于变量同步
很多人谈论 PHP 会话同步就像它是一个变量同步,一旦你覆盖变量值,写入内存位置就会发生,并且任何脚本中的下一次读取都会获取新值。正如我们从澄清 #1 中看到的 - 这不是真的。该脚本在整个脚本中使用在脚本开始时读取的值,即使其他一些脚本更改了这些值,正在运行的脚本在下次刷新之前不会知道新值。这是非常重要的一点。
另外,请记住,即使使用 PHP 大锁定,会话中的值也会发生变化。说“首先完成的脚本将覆盖值”之类的说法不是很准确。值变化还不错,我们追求的是不一致,即不应该在我不知情的情况下发生变化。
澄清 5:我们真的需要大锁吗?
现在,我们真的需要 Big Lock(请求级别)吗?与数据库隔离的情况一样,答案是它取决于您想要如何做事。对于 $_SESSION 的默认实现,恕我直言,只有大锁才有意义。如果我要使用我在整个脚本开头读到的值,那么只有大锁才有意义。如果我将 $_SESSION 实现更改为“始终”获取“新鲜”值,那么您不需要 BIG LOCK。
假设我们实现了一个会话数据版本控制方案,如对象版本控制。现在,脚本 2 写入将成功,因为脚本 1 尚未到达写入点。脚本 2 写入会话存储并将版本增加 1。现在,当脚本 1 尝试写入会话时,它将失败(第 5 行) - 我认为这是不可取的,尽管可行。
====================================
从 (1) 和 (2) 可以看出,无论您的脚本多么复杂,X 读取和 Y 写入会话,
- 会话处理程序 read() 和 write() 方法只调用一次
- 它们总是被调用
现在,网上有一些自定义 PHP 会话处理程序尝试执行“变量”级锁定等。我仍在尝试找出其中的一些。但是我不赞成复杂的方案。
假设带有 $_SESSION 的 PHP 脚本应该服务于网页并在几毫秒内处理,我认为额外的复杂性是不值得的。 Like Peter Zaitsev mentions here,写入后选择更新并提交应该可以解决问题。
这里我包含了我为实现锁定而编写的代码。用一些“比赛模拟”脚本来测试它会很好。我相信它应该工作。我在网上找到的正确实现并不多。如果您能指出错误,那就太好了。我用裸 mysqli 做到了这一点。
<?php
namespace com\indigloo\core {
use \com\indigloo\Configuration as Config;
use \com\indigloo\Logger as Logger;
/*
* @todo - examine row level locking between read() and write()
*
*/
class MySQLSession {
private $mysqli ;
function __construct() {
}
function open($path,$name) {
$this->mysqli = new \mysqli(Config::getInstance()->get_value("mysql.host"),
Config::getInstance()->get_value("mysql.user"),
Config::getInstance()->get_value("mysql.password"),
Config::getInstance()->get_value("mysql.database"));
if (mysqli_connect_errno ()) {
trigger_error(mysqli_connect_error(), E_USER_ERROR);
exit(1);
}
//remove old sessions
$this->gc(1440);
return TRUE ;
}
function close() {
$this->mysqli->close();
$this->mysqli = null;
return TRUE ;
}
function read($sessionId) {
Logger::getInstance()->info("reading session data from DB");
//start Tx
$this->mysqli->query("START TRANSACTION");
$sql = " select data from sc_php_session where session_id = '%s' for update ";
$sessionId = $this->mysqli->real_escape_string($sessionId);
$sql = sprintf($sql,$sessionId);
$result = $this->mysqli->query($sql);
$data = '' ;
if ($result) {
$record = $result->fetch_array(MYSQLI_ASSOC);
$data = $record['data'];
}
$result->free();
return $data ;
}
function write($sessionId,$data) {
$sessionId = $this->mysqli->real_escape_string($sessionId);
$data = $this->mysqli->real_escape_string($data);
$sql = "REPLACE INTO sc_php_session(session_id,data,updated_on) VALUES('%s', '%s', now())" ;
$sql = sprintf($sql,$sessionId, $data);
$stmt = $this->mysqli->prepare($sql);
if ($stmt) {
$stmt->execute();
$stmt->close();
} else {
trigger_error($this->mysqli->error, E_USER_ERROR);
}
//end Tx
$this->mysqli->query("COMMIT");
Logger::getInstance()->info("wrote session data to DB");
}
function destroy($sessionId) {
$sessionId = $this->mysqli->real_escape_string($sessionId);
$sql = "DELETE FROM sc_php_session WHERE session_id = '%s' ";
$sql = sprintf($sql,$sessionId);
$stmt = $this->mysqli->prepare($sql);
if ($stmt) {
$stmt->execute();
$stmt->close();
} else {
trigger_error($this->mysqli->error, E_USER_ERROR);
}
}
/*
* @param $age - number in seconds set by session.gc_maxlifetime value
* default is 1440 or 24 mins.
*
*/
function gc($age) {
$sql = "DELETE FROM sc_php_session WHERE updated_on < (now() - INTERVAL %d SECOND) ";
$sql = sprintf($sql,$age);
$stmt = $this->mysqli->prepare($sql);
if ($stmt) {
$stmt->execute();
$stmt->close();
} else {
trigger_error($this->mysqli->error, E_USER_ERROR);
}
}
}
}
?>
要注册对象会话Handler,
$sessionHandler = new \com\indigloo\core\MySQLSession();
session_set_save_handler(array($sessionHandler,"open"),
array($sessionHandler,"close"),
array($sessionHandler,"read"),
array($sessionHandler,"write"),
array($sessionHandler,"destroy"),
array($sessionHandler,"gc"));
ini_set('session_use_cookies',1);
//Defaults to 1 (enabled) since PHP 5.3.0
//no passing of sessionID in URL
ini_set('session.use_only_cookies',1);
// the following prevents unexpected effects
// when using objects as save handlers
// @see http://php.net/manual/en/function.session-set-save-handler.php
register_shutdown_function('session_write_close');
session_start();
这是使用 PDO 完成的另一个版本。这个检查是否存在 sessionId 并进行更新或插入。我还从 open() 中删除了 gc 函数,因为它不必要地在每次页面加载时触发 SQL 查询。陈旧的会话清理可以通过 cron 脚本轻松完成。如果您使用的是 PHP 5.x,这应该是要使用的版本。如果您发现任何错误,请告诉我!
==========================================
namespace com\indigloo\core {
use \com\indigloo\Configuration as Config;
use \com\indigloo\mysql\PDOWrapper;
use \com\indigloo\Logger as Logger;
/*
* custom session handler to store PHP session data into mysql DB
* we use a -select for update- row leve lock
*
*/
class MySQLSession {
private $dbh ;
function __construct() {
}
function open($path,$name) {
$this->dbh = PDOWrapper::getHandle();
return TRUE ;
}
function close() {
$this->dbh = null;
return TRUE ;
}
function read($sessionId) {
//start Tx
$this->dbh->beginTransaction();
$sql = " select data from sc_php_session where session_id = :session_id for update ";
$stmt = $this->dbh->prepare($sql);
$stmt->bindParam(":session_id",$sessionId, \PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
$data = '' ;
if($result) {
$data = $result['data'];
}
return $data ;
}
function write($sessionId,$data) {
$sql = " select count(session_id) as total from sc_php_session where session_id = :session_id" ;
$stmt = $this->dbh->prepare($sql);
$stmt->bindParam(":session_id",$sessionId, \PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
$total = $result['total'];
if($total > 0) {
//existing session
$sql2 = " update sc_php_session set data = :data, updated_on = now() where session_id = :session_id" ;
} else {
$sql2 = "insert INTO sc_php_session(session_id,data,updated_on) VALUES(:session_id, :data, now())" ;
}
$stmt2 = $this->dbh->prepare($sql2);
$stmt2->bindParam(":session_id",$sessionId, \PDO::PARAM_STR);
$stmt2->bindParam(":data",$data, \PDO::PARAM_STR);
$stmt2->execute();
//end Tx
$this->dbh->commit();
}
/*
* destroy is called via session_destroy
* However it is better to clear the stale sessions via a CRON script
*/
function destroy($sessionId) {
$sql = "DELETE FROM sc_php_session WHERE session_id = :session_id ";
$stmt = $this->dbh->prepare($sql);
$stmt->bindParam(":session_id",$sessionId, \PDO::PARAM_STR);
$stmt->execute();
}
/*
* @param $age - number in seconds set by session.gc_maxlifetime value
* default is 1440 or 24 mins.
*
*/
function gc($age) {
$sql = "DELETE FROM sc_php_session WHERE updated_on < (now() - INTERVAL :age SECOND) ";
$stmt = $this->dbh->prepare($sql);
$stmt->bindParam(":age",$age, \PDO::PARAM_INT);
$stmt->execute();
}
}
}
?>