- r - 以节省内存的方式增长 data.frame
- ruby-on-rails - ruby/ruby on rails 内存泄漏检测
- android - 无法解析导入android.support.v7.app
- UNIX 域套接字与共享内存(映射文件)
我正在尝试在 PHP + MySQL 中实现自定义 session 持久器。大多数东西都是微不足道的 - 创建你的数据库表,创建你的读/写函数,调用 session_set_save_hander()
等。甚至还有一些教程可以为您提供示例实现。但不知何故,所有这些教程都方便地忽略了关于 session 持久器的一个小细节 - 锁定 .现在这才是真正有趣的开始!
我看了session_mysql的实现PHP 的 PECL 扩展。使用 MySQL 的函数 get_lock()
和 release_lock()
.看起来不错,但我不喜欢它的做法。锁在读函数中获取,在写函数中释放。但是如果 write 函数永远不会被调用呢?如果脚本以某种方式崩溃,但 MySQL 连接保持打开状态(由于池或其他原因)怎么办?或者如果脚本进入致命的僵局怎么办?
我只是had a problem脚本打开一个 session ,然后尝试 flock()
NFS 共享上的文件,而另一台计算机(托管该文件)也在做同样的事情。结果是flock()
-over-NFS 调用在每次调用时阻塞脚本大约 30 秒。它处于 20 次迭代的循环中!由于这是一个外部操作,PHP 的脚本超时不适用,并且每次访问此脚本时 session 都会被锁定超过 10 分钟。而且,幸运的是,这是每 5 秒由 AJAX 留言箱轮询一次的脚本……主要的表演者。
我已经对如何以更好的方式实现它有了一些想法,但我真的很想听听其他人的建议。我对 PHP 没有太多经验,不知道有哪些微妙的边缘情况在阴影中隐约可见,有朝一日可能会危及整个事情。
补充:
好吧,似乎没有人有什么建议。好的,这是我的想法。我想就这可能出错的地方提出一些意见。
INSERT IGNORE INTO sessions (id, data, lastaccesstime, locktime, lockid) values ($sessid, null, now(), null, null);
- 如果 session 行不存在,这将创建 session 行,但如果它已经存在,则不执行任何操作; UPDATE sessions SET (lastaccesstime, locktime, lockid) values (now(), now(), $guid) where id=$sessid and (lockid is null or locktime < date_add(now(), INTERVAL -30 seconds));
- 这是一个原子操作,它将获取 session 行上的锁(如果它没有被锁定或锁已过期),或者什么都不做。 mysql_affected_rows()
是否获得了锁。如果已获得 - 继续。如果不是 - 每 0.5 秒重新尝试操作一次。如果 40 秒后仍未获得锁,则抛出异常。 UPDATE sessions SET (lastaccesstime, data, locktime, lockid) values (now(), $data, null, null) where id=$sessid and lockid=$guid;
这是另一个原子操作,它将用新数据更新 session 行,如果仍然有锁,则删除锁,但如果锁已被取走,则不执行任何操作。 gc
请求操作,只需删除带有 lastaccesstime
的所有行太老。 最佳答案
好的。答案会更长一点 - 所以要有耐心!
1)无论我要写什么,都是基于我过去几天所做的实验。可能有一些我可能不知道的旋钮/设置/内部工作。如果您发现错误/或不同意,请大声喊叫!
2)首先澄清 - 何时读取和写入 session 数据
即使您的脚本中有多个 $_SESSION 读取, session 数据也将被读取一次。从 session 读取是基于每个脚本的。此外,数据获取是基于 session_id 而不是键发生的。
2)第二个澄清 - 写总是在脚本末尾调用
A) 写入 session save_set_handler 总是被触发,即使对于只从 session “读取”而从不进行任何写入的脚本也是如此。
B) 写入仅触发一次,在脚本结束时或如果您显式调用 session_write_close。同样,写入基于 session_id 而不是键
3)第三个澄清:为什么我们需要锁定
<?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);
}
}
}
}
?>
$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();
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();
}
}
}
?>
关于php - 如何在 PHP + MySQL 中正确实现自定义 session 持久器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1022416/
我是一名优秀的程序员,十分优秀!