Хорошо. Ответ будет немного длиннее - так что терпение!
1) Все, что я собираюсь написать, основано на экспериментах, которые я провел за последние пару дней. Могут быть какие-то ручки / настройки / внутренняя работа, о которых я не могу знать. Если вы заметили ошибки или не согласны, пожалуйста, кричите!
2) Первое уточнение - КОГДА ДАННЫЕ СЕССИИ ПРОЧИТАЮТСЯ и ЗАПИСАНЫ
Данные сеанса будут прочитаны ровно один раз, даже если у вас есть несколько чтений $ _SESSION внутри вашего скрипта. Чтение из сеанса является для каждого сценария. Более того, выборка данных происходит на основе session_id, а не ключей.
2) Второе уточнение - НАПИСАНИЕ ВСЕГДА ВЫЗЫВАЕТСЯ В КОНЦЕ СЦЕНАРИИ
A) Запись в сеанс save_set_handler всегда запускается, даже для сценариев, которые только «читают» из сеанса и никогда не делают никаких записей.
Б) Запись запускается только один раз, в конце скрипта или если вы явно вызываете session_write_close. Опять же, запись основана на session_id, а не на ключах
3) Третье уточнение: ПОЧЕМУ НАМ НУЖНА Блокировка
- Что это за суета?
- Нам действительно нужны блокировки на сеансе?
- Нужна ли нам упаковка большого замка ЧИТАТЬ + ЗАПИСАТЬ
Объяснить суету
script1
- 1: $ x = S_SESSION ["X"];
- 2: сон (20);
- 3: если ($ x == 1) {
- 4: // сделать что-то
- 5: $ _SESSION ["X"] = 3;
- 6:}
- 4: выход;
Сценарий 2
- 1: $ x = $ _SESSION ["X"];
- 2: if ($ x == 1) {$ _SESSION ["X"] = 2; }
- 3: выход;
Несоответствие заключается в том, что сценарий 1 выполняет что-то на основе значения переменной сеанса (строка: 3), которое изменилось другим сценарием, когда сценарий 1 уже выполнялся. Это пример скелета, но он иллюстрирует суть. Тот факт, что вы принимаете решения, основанные на чем-то, что уже не ПРАВДА.
при использовании блокировки сеанса PHP по умолчанию (блокировка уровня запроса) script2 будет блокироваться в строке 1, поскольку он не может прочитать файл, который скрипт 1 начал читать в строке 1. Таким образом, запросы к данным сеанса сериализуются. Когда script2 читает значение, оно гарантированно читает новое значение.
Разъяснение 4: СИНХРОНИЗАЦИЯ СЕССИИ PHP ОТЛИЧАЕТСЯ ОТ ПЕРЕМЕННОЙ СИНХРОНИЗАЦИИ
Многие люди говорят о синхронизации сессий PHP, как если бы это было похоже на синхронизацию переменных, запись в память происходит, как только вы перезаписываете значение переменной, а следующее чтение в любом скрипте извлечет новое значение. Как мы видим из уточнения № 1 - это не так. Сценарий использует значения, прочитанные в начале сценария по всему сценарию, и даже если какой-либо другой сценарий изменил значения, работающий сценарий не будет знать о новых значениях до следующего обновления. Это очень важный пункт.
Кроме того, имейте в виду, что значения в сеансе изменяются даже при большой блокировке PHP. Сказать что-то вроде «скрипт, который первым заканчивает, перезапишет значение», не очень точно. Изменение значения не плохо, то, что мы ищем, это несоответствие, а именно, оно не должно меняться без моего ведома.
УТОЧНЕНИЕ 5: ДЕЙСТВИТЕЛЬНО НУЖНО БОЛЬШОЙ БЛОКИРОВКА?
Теперь, действительно ли нам нужен Big Lock (уровень запроса)? Ответ, как и в случае изоляции БД, заключается в том, что это зависит от того, как вы хотите что-то делать. С реализацией по умолчанию $ _SESSION, IMHO, имеет смысл только большая блокировка. Если я собираюсь использовать значение, которое я прочитал в начале всего скрипта, тогда имеет смысл только большая блокировка. Если я изменю реализацию $ _SESSION на «всегда», выбираю «свежее» значение, тогда вам не понадобится БОЛЬШАЯ БЛОКИРОВКА.
Предположим, мы реализуем схему управления версиями данных сеанса, например, управление версиями объекта. Теперь запись сценария 2 будет успешной, потому что сценарий 1 еще не пришел к точке записи. script-2 записывает в хранилище сеансов и увеличивает версию на 1. Теперь, когда скрипт 1 пытается записать в сеанс, он завершится неудачей (строка 5) - я не думаю, что это желательно, хотя выполнимо.
===================================
Из (1) и (2) следует, что независимо от того, насколько сложен ваш скрипт, с X читает и Y записывает в сессию,
- методы обработчика сессии read () и write () вызываются только один раз
- и они всегда называются
Теперь в сети есть пользовательские обработчики сеансов PHP, которые пытаются выполнить блокировку на «переменном» уровне и т. Д. Я все еще пытаюсь выяснить некоторые из них. Однако я не сторонник сложных схем.
Предполагая, что PHP-скрипты с $ _SESSION должны обслуживать веб-страницы и обрабатываться за миллисекунды, я не думаю, что дополнительная сложность того стоит. Как Петр Зайцев упоминает здесь , выбор для обновления с коммитом после записи должен сработать.
Здесь я включаю код, который я написал для реализации блокировки. Было бы неплохо протестировать его с помощью некоторых скриптов "Race simulation". Я считаю, что это должно работать. В сети не так много правильных реализаций. Было бы хорошо, если бы вы могли указать на ошибки. Я сделал это с голыми 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);
}
}
}
}
?>
Чтобы зарегистрировать обработчик сеанса объекта,
$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 и выполняет обновление или вставку. Я также удалил функцию gc из open (), так как она без необходимости запускает 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();
}
}
}
?>