Пишем свою реализацию сессий для обработки мертвой сессии перед зачисткой

    Мой первый хабратопик, надеюсь, что не последний.

    Представим ситуацию: есть корзина покупок на сайте, при добавлении в корзину мы ставим на товар т.н. lock, исключающий его из списка доступных для покупки товаров. Когда клиент удаляет товар из корзины — lock снимается. Но что делать, если пользователь просто закрыл браузер? В таком случае сессия будет удалена сборщиком мусора, а локи так и останутся.

    Когда я столкнулся с такой ситуацией, первое что мне пришло в голову — хранить локи и дату доступа в БД и периодически дергать её кроном. Но костыльность этого решения очевидна. А вот ещё бред, с которым я столкнулся при решении сабжа: для сериализации и десереализации сессий используются функции и формат, отличные от функций serialize и unserialize. Приходится делать велосипеды для ансериализации сессии.

    Ближе к телу: как решил проблему я…
    Тут надо сделать замечание, что идею для такого решения мне подал хабраюзер rigid, а помогли решить пару около-сабжевых проблем в конференции php@conference.jabber.ru.

    PHP позволяет определять свои функции для обработки сессий. Отвечает за это функция session_set_save_handler. В качестве параметров она принимает список функций, который будут вызываться для работы с сессиями. В мануале есть даже пример, который реализует стандартный механизм работы с сессиями. Его-то мы и возьмем, изменив только функцию gc, которая занимается сборкой мусора, т.е. удалением файлов мертвых сессий.

    Пример функции gc:

    /* Функция принимает в качестве параметра время жизни сессии */
    function gc($maxlifetime)
     {
        global $sess_save_path; /* путь, где лежат сессии */
        foreach (glob("$sess_save_path/sess_*") as $filename)
         {
            /* Проверяем не пора ли убить сессию */
            if (filemtime($filename) + $maxlifetime < time())
             {
                $tmp_sess=unserializesession(file_get_contents($filename)); /* $tmp_sess у нас теперь аналогична $_SESSION той сессии */
                /* Обрабатываем данные, например снимаем локи из этой сессии */
                @unlink($filename); /* Удаляем сессию */
             }
         }
        return true;
     }


    * This source code was highlighted with Source Code Highlighter.


    Код функции unserializesession, взятый откуда-то из интернета (скорее всего из комментариев к функции в мануале PHP):

    function unserializesession($data) {
     $vars=preg_split('/([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff^|]*)\|/',
              $data,-1,PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
     for($i=0; $vars[$i]; $i++) $result[$vars[$i++]]=unserialize($vars[$i]);
     return $result;
    }


    * This source code was highlighted with Source Code Highlighter.


    Теперь подключаем это в наш проект:

    session_set_save_handler("open", "close", "read", "write", "destroy", "gc");
    /* Вероятность чистки мусора на каждый session_start() примерно равна 30%, другими словами - чистка мертвых сессий будет производится при тридцати вызовах session_start() из ста */
    ini_set("session.gc_probability", 30); /* Можно настроить на 100%, если у вас там нет никакого медленного кода */
    ini_set("session.gc_divisor", 100);
    ini_set("session.gc_maxlifetime", 1800); /* Время жизни сессии в секундах (то самое, которое передается в функцию gc) */
    session_start();


    * This source code was highlighted with Source Code Highlighter.


    Есть одно но: в Debian/Ubuntu свой механизм очистки сессий, который выполняется кроном, а у PHP нет возможности удалять файлы сессий. Мне это не понравилось, т.к. ломает функционал PHP подменяя его своим сборщиком мусора для сессий. Решить проблему можно задав собственный каталог для файлов сессии и закрыв его в .htaccess (если он находится в document_root).

    P.S. Честно говоря я не уверен, что мне можно было публиковать пост сразу в блог PHP. Я не активный пользователь хабра и не знаю местных порядков. Прошу не ругаться.
    Поделиться публикацией

    Похожие публикации

    Комментарии 53

    • НЛО прилетело и опубликовало эту надпись здесь
        0
        «черезжопа» — это перебор. Но все-таки этот «воркэраунд» ломает сабжевую возможность php — берет на себя работу сборщика мусора. Конечно, это очень редко применяемая функция, так что «воркэраунд» имеет право на жизнь.
        Но если ты пишешь скрипт, который будет работать на сервере Debian/Ubuntu без прав администрирования, приходится делать другой воркэраунд — хранить сессии в не очень положенном для этого месте.
          +1
          Убрал «черезжопу» заменив более лояльным «мне не понравилось».
            +1
            Зря! «Черезжопа», вот именно так, одним словом — офигительный термин :) И главное, суть отражает. Может не в этом контексте, но вообще)
        +2
        когда изменял обработчик для записи бд, обнаружил, что mysqlirealescapestring каким-то образом умудряется портить сериализованные данные в некоторых случаях. Я не стал особо искать причину и поменял mysqlirealescapestring на base64_encode. Это просто так… в тему
          +1
          Лично я использую объект, методы которого передаются в session_set_save_handler.
          Вместо mysqlirealescapestring использую prepare->execute и ничего не портиться. А также ненужные сессии удаляются пользователем, зашедшим первый раз на сайт. То есть если у него нет куки, он запускает запрос удаления устаревших сессий.
            0
            Ну и совсем небольшая такая заметочка от человека далекого от php — переименовать unserializesession в deserializeSession, ну или какой там в php принят кейс.
              0
              Менять unserialize на deserialize не целесообразно т.к. в PHP есть пара функций serialize/unserialize.
              А по поводу Session — мне кажется это не важно. Функция «чужая» и я скопировал её как есть, исключительно как пример.

              Спасибо за фидбэк.
                +3
                Кстати, в PHP есть функция session_decode, чтобы ей воспользоваться достаточно поменять текущий идентификатор сессии на нужный (функцией session_id).
              +2
              А может, просто не ставить локи?
                0
                Есть целая куча ситуаций, когда локи нужны и когда их удобней (быстрей и проще) держать в сессиях, чем в бд.
                Например такой представитель жанра как скандинавские аукционы — много конкурирующих посетителей, тыкающих на кнопочку «купить».
                Да и в посещаемом магазине может получиться ситуация, когда два пользователя одновременно оплачивают последнюю единицу конкретного продукта — хоть какие-то блокировки точно должны быть.
                  +1
                  А где я прописал, что они должны быть в БД? Мне кажется, вы сами себе создаете проблему, потому что исходите из предположения «хоть какие-то блокировки точно должны быть». Нет блокировок — нет проблем.
                    0
                    Нет блокировок — есть проблемы. То, что клиент узнает об отсутствии товара на стадии, когда сервис webmoney опрашивает магазин на возможность совершения платежа — это проблема.
                    Или как вы спроектируете приложение без блокировок и без описанной проблемы?
                    P.S. Плохой знак — я промахнулся два раза плюсанув ваши комментарии =)
                      –9
                      Я спроектирую приложение без блокировок и без описанной проблемы, когда увижу точные спеки. Но тут встает проблема другого рода — мой рейт в 30$/hr =)

                      P.S. Больше не промахивайтесь =)
                        0
                        Мне кажется то, что вы спроектируете, так или иначе и будет блокировками, просто другой их реализацией ;)
                          –2
                          Тут я не буду вас переубеждать: вы вольны представлять все, что вам угодно; SRS нет и я еще ничего не предложил
                          0
                          Кстати, тут можно плюнуть на локи.
                          На самом деле идея топика была обратить внимание на возможность обработать мертвую сессию перед её удалением для тех, кто эту возможность ещё не видел.
                            +3
                            А зачем может в нормальном flow понадобиться обрабатывать мертвые сессии? Поймать событий on-destroy-session? Не уверен, что на это стоит завязывать логику по 2м причинам:

                            1. (общая) session kill — это не настолько reliable в web app, чтобы на это можно было надеяться 100%.

                            2. (частная) ваш алгоритм восстановления сессии из dehydrated state сдохнет сразу, как только php поменяет формат хранения сессий (например, при переходе от версии x.y.z к x.y.z+1). Это же private implementation details, они вольны это делать в любое время.
                            При этом самое страшное не то, что сессиий перестанут восстанавливаться, если формат сменится сильно, а то, если они продолжит восстанавливаться, но с мелкими дефектами типа порчи пары байтов и все такое. Это будет малозаметный fail, которые конкретно попортит нервы в случае электронного магазина и его клиентов.

                            Как-то так.
                              –1
                              Вынужден признать — ваш второй комментарий заставил меня задуматься. Хотя я бы отнес сохранение формата сессии к сохранению обратной совместимости, а может и нет, как вы сказали — private implementation details имеет место быть, в мануале ведь не описано =(
                            0
                            Ололо; господа анонимусы, нещадно херачащие карму, не желают пояснить, с хуяли они это делают? Или они знают, как предлагать решения, в отсутствие SRS?
                              0
                              За демпинг, наверное :)
                    0
                    Хочу добавить от себя. Когда автор хабратопика обратился ко мне, вопрос стоял даже не о конкретно локах. Вопрос встал — отследить ушедшего с сайта юзера (штатное закрытие браузера, аварийный останов, etc.). Мной было предложено изящное и, на мой взгляд, очевидное решение. Суть решения: описываем некий класс, в котором бы хранились все необходимые сессионнные данные. В том же классе описываем деструктор, выполняющий все необходимые действия для очистки сессиных данных, в том числе и овобождение локов в данном конкретном случае. Вариант оказался неработоспособным, т.к. ПХП хранит сессионные даные в сериализованном формате в файлах и, при уничтожении сессии. прсто грохает эти файлы. То есть, даже если я сохраню в $_SESSION[«bla»] экземпляр некого класса, его деструктор при уничтожении сессии не отработает. Это, как мне кажется, не совсем логично — ООП учит, что при уничтожении объекта отрабатывает деструктор.
                    Хотя, может быть, это уже сказывается влияние на меня Java-подхода к веб-программированию.
                      0
                      А расскажите тогда, пожалуйста вот что:
                      а) в какой знакомой вам системе с ООП у сериализованных в персистентный поток объектов вызываются деструкторы, раз вы про них решили вспомнить?
                      б) что такое «java-подход к веб-программированию»?
                        0
                        а) не уверен на 100% — самому лично это не было еще нужно. Вообще считаю логичным с точки зрения ООП — если объект был создан, его надо корректно убить, вызвав деструктор — не важно, убивает его сам разработчик явным вызовом или он уубиваестя GC. Это выглядит ожидаемым и логичным. Я не прав?
                        б) ява-подход в любом ее проялвении — все есть объект (экземпляр некоего класса, потомка java.lang.Object). в том числе и в веб-разработке.

                        ЗЫ. Хранение персистентных данных в сериализованном виде в файлах считаю извращением. Это простительно для CGI, Но непростительно для mod_php и подобных.
                          0
                          1. >>> описываем некий класс, в котором бы хранились все необходимые сессионнные данные. В том же классе описываем деструктор, выполняющий все необходимые действия для очистки сессиных данных, в том числе и овобождение локов в данном конкретном случае. Вариант оказался неработоспособным, т.к. ПХП хранит сессионные даные в сериализованном формате в файлах и, при уничтожении сессии. прсто грохает эти файлы
                          ^------- Я вот применительно к этому спрашивал — если для вас логично, что для объекта вызовется деструктор, то мне интересно, где вы видели такое для сериализованных объектов.
                          Кстати, в managed environment нет деструкторов для любых объектов, управляемых GC, или вызов их не гарантируется.

                          2. Все есть объект появилось в smalltalk, а это было лет так за 20 до java. Тема web-программирования в вашем комменте не раскрыта, или я ошибаюсь? А файл на диске есть объект, и если его удалить, вызовется его деструктор?

                          ЗЫ. Вполне нормально решение. В java такое тоже можно делать.
                            0
                            1. Я изначально рассуждал в понятиях ООП вообще. И поэтому мнесейчас странно узнавать, что такое может ыбть — объект просто удаляется без выхова соответствующих методов. Мне это кажется неправильным. Наприер сейас на этом у принципе у меня спланировано одно приложение (еще не реализовано) ПО выходу с отпуска обязательно проверю, получится ли реализовать именно на этом.Я даже мысли не допускал, что деструктор может не вызваться.
                            2. Файл на диске — объект ОС. Если вы опишете в вашем любимом языке класс File и реализуете у него деструктор, при уничтожении экземпляра этого класса File, его деструктор отработает, не?

                            И изврат даже не в том, что среда исполнения позволяет себе хранить персистентные данные в файлах — это в самом деле просто одна из реализаций, фиг бы с ним. Мне кажется недопустимым просто тупое удаление объектов без вызовов соответствуюих методов.
                              0
                              А, я вас понял.

                              Ну, если вы писали на Java, вы должны быть в курсе, что деструкторов там нет, есть файналайзеры (finalizers), вызов которых, впрочем, не гарантирован — т.е. они могут быть вызваны, а могут быть и нет; надеяться на их вызов не стоит, максимум — аварийное освобождение некоторых ресурсов, если они не были освобождены.

                              Та же самая петрушка и в чистом .net — объекты, управляемые GC, уничтожаются без предупреждения, с финализаторами абсолютно идентично джаве.

                              Насчет php не могу сказать, потому что никогда на нем не писал и не сталкивался; однако наслышан про чертей и болото в нем, так что не удивлюсь любой нелогичной реализации :)
                                0
                                java.sun.com/j2se/1.4.2/docs/api/java/io/ObjectOutputStream.html
                                Classes that require special handling during the serialization and deserialization process must implement special methods with these exact signatures:

                                private void readObject(java.io.ObjectInputStream stream)
                                     throws IOException, ClassNotFoundException;

                                private void writeObject(java.io.ObjectOutputStream stream)
                                     throws IOException


                                Только что проверил на Java 1.4.2_17 — вот этот код сработал:
                                import java.io.FileOutputStream;
                                import java.io.IOException;
                                import java.io.ObjectOutputStream;
                                import java.io.Serializable;
                                
                                public class SerializationCallbackTest implements Serializable {
                                  private void writeObject(ObjectOutputStream s)
                                      throws IOException {
                                    System.out.println("writeObject called");
                                  }
                                
                                  public static void main(String[] args) {
                                    try {
                                      SerializationCallbackTest me = new SerializationCallbackTest();
                                      FileOutputStream fos = new FileOutputStream("C:\\t.tmp");
                                      ObjectOutputStream oos = new ObjectOutputStream(fos);
                                      oos.writeObject(me);
                                      oos.close();
                                      fos.close();
                                    } catch (Exception e) {
                                      e.printStackTrace();
                                    }
                                  }
                                }


                                Дальше голос свыше говорит:
                                Serialization of an object can be prevented by implementing writeObject and readObject methods that throw the NotSerializableException. The exception will be caught by the ObjectOutputStream and abort the serialization process.

                                Так что если аккуратненько почистить за собой в writeObject(), то и хвостов не оставишь, и сериализацию не сломаешь.
                                  0
                                  Эээ нет, так не пойдет.
                                  Я знаю, как обслуживать сериализацию/десериализацию объектов; вопрос в другом был — при инвалидации сессии в php сериализованный контент сессии просто выбросится, без всяких подъемов в память и выполнений очистки, даже если она есть.
                                    +1
                                    Я ответил только по Java, за PHP сказать не могу, т.к. не в курсе.
                                    И даже если такого гарантированного механизма нет (PHP, Java, Python, что хотите), его надо придумать и протолкнуть, особенно в языках, традиционно близких к Web, потому что событие истечения срока жизни сессии достаточно важное, чтобы его не игнорировать и иметь возможность обработать штатно.
                                      0
                                      В Java Servlet API есть SessionListener, в котором можно определить метод sessionDestroyed. Других велосипедов не нужно.
                                        0
                                        Нужно, если вы в кластере — у SessionListener есть проблемы с ним
                                          0
                                          Я так понимаю это проблемы конкретных реализаций? В любом случае спасибо за информацию, о таких проблемах был не в курсе.
                        0
                        Вроде бы, всё как раз логично.
                        Вы создали объект, сериализировали его, записали в сессию.
                        Деструктор сработает при уничтожении объекта — когда ваша программа завершится.
                        Сессия же убивается, когда объект лежит замороженный в сериализованном виде в файлике. (При этом не обязательно запускаеть интерпретатор или виртуальную машину, убить сессию может и вебсервер и cron-job)
                        В php есть __sleep() сериализации.
                        Я как-то делал свой велосипед для сериализации и долговременного хранения объектов, причём объект создавался на одном сервере, записывался в сериализированном виде в базу данных, а когда-нибудь потом загружался на другом сервере и выполнялся.

                        Вам можете сделать свою реализацию сессии, а потом по крону «размораживать» прокисшие сессии, выполнять у них деструктор и удалять.
                        0
                        как вариант, табличка «лог покупок», где ставится дата экспайра = текущая плюс лайфтайм сессии, так кстати честнее, время очистки сессии не равно времени ее жизни, особенно если мало посещений
                          0
                          Подобный подход описан в книге «PHP5 для профессионалов» Леки-Томпсона, Коува, Новицки и др.

                          Ну а мы у нас на работе сессии вообще не используем. Не принято как-то.
                            –1
                            Напомните название магазина? Продам вашим конкурентам скрипт, который все разом положит в корзину. ;)
                              +2
                              плохой способ локов, лучше ставить время истечения, и перепроверять. а так получается зависимое решение, вдруг мусор уберётся раньше другим скриптом, где уборщик не переписан, или сменится сторадж с файлов на бд или мемкэш, да и парсить кучу сессий, чтобы убрать лок на товар — не кошерно.
                                0
                                Согласен — решение сильно зависит от окружения. Но вариант обрабатывать сессии таким вот образом мне показался интересным а пример с локами — для демонстрации.
                                –2
                                А заказы, которые пошли в обработку логистами вы тоже в таблице сессий что ли храните?
                                  0
                                  А что мешает работать со своим классом хранения сессий?
                                  Его расширять можно без таких вот проблем.
                                    0
                                    если делать через базу — то проблем не будет.
                                    1. «поместить в корзину» --> в базе прописываем этот факт и дату.
                                    2. загрузки страниц пользователя сопровождаются «обновлением» его дат корзины.
                                    3. показываем только те товары, которые не в корзине (с учетом дат, например в которых время меньше текущего на 10 минут и более)
                                      0
                                      Я так понимаю, что событие о том, что сессия кончилась будет выполняться через некоторое время в момент других запросов от иных пользователей? Получается, что если запросов к PHP нет, то и сессия будет все еще жить. В вашем случае может это приемлемо, но по-хорошему следует писать cron-скрипт и свой провайдер сессий.
                                        0
                                        Да, правильно понимаете.
                                        Ну а почему не приемлемо, для простенького магазинчика самое оно: если поставить чистку на каждый старт сессии, то кто бы не зашёл — админ или клиент — локи будут в актуальном состоянии.
                                        Да и не обязательно это для локов применять, можно и вовсе не применять. Но вы ведь судя по вопросу до прочтения этого не знали, теперь знаете — хоть какая-то польза от заметки есть.
                                          0
                                          А событие по очистке выполнится в начале выполнения запроса или в конце?
                                            0
                                            Во время вызова session_start()
                                        0
                                        Такое большое количество слов «сессия», особенно перед учебной сессией =)
                                          0
                                          Храните ваши сессии в сберегательной банке. В смысле базе. В смысле в сберегательном мемкеше :)
                                            0
                                            Я бы разделял обработчики для пользователей и служебные. В частности для данной задачи можно написать на PHP простенький cron скрипт. А то получается, что пользователи открывая ваш сайт инициируют сборщик мусора.
                                              0
                                              Вообще я храню все сессии в БД, вместо файлов, а очищаю так:

                                              // Сборка мусора — ищем все старые файлы и удаляем их
                                              function ses_gc($maxlifetime) {
                                              global $db_prefix;
                                              $now = time();
                                              $sql = «DELETE FROM ».$db_prefix.«session WHERE ($now-lifetime)>$maxlifetime»;
                                              $q = mysql_query($sql) or die(«Ошибка при работе сессии!»);
                                              return true;
                                              }
                                                0
                                                Добавьте таймаут на блокировку (опционально — можно продлевать при каждом обращении с корзиной). Это позволит абстрагироваться от привязки к реализации сессий.
                                                  +1
                                                  Храню сессии в HEAP-таблице MySQL (в памяти), выполняю DELETE FROM user_sessions WHERE expires_at < NOW() при каждом запросе, в cookie храню user_session_id и user_session_key (md5 от «секрета» сессии, меняется при критических операциях). Всё.

                                                  Успехов.
                                                  • НЛО прилетело и опубликовало эту надпись здесь

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                  Самое читаемое