Düzgün PHP + MySQL bir özel oturum persister uygulamak nasıl?

4 Cevap php

PHP + MySQL bir özel oturum persister uygulamak çalışıyorum. Şeyler çoğu önemsiz - çeşitli dersler sizin için teklif örnek uygulamalar orada bile vardır sizin okuma / yazma fonksiyonları, çağrı session_set_save_hander(), vb yapmak, DB tablo oluşturun. Ama nedense bütün bu dersler uygun seans persisters hakkında bir küçük ayrıntı gözden kaçan - locking. Gerçek eğlence başlar ve artık o!

Ben PHP session_mysql PECL uzatma uygulanması baktı. Bu MySQL fonksiyonlarını get_lock() ve release_lock() kullanır. Güzel görünüyor, ama ben bunu yapıyor beğenmiyorum. Kilit read fonksiyonu edinilen ve write işlevinde salınır. Ama yazma işlevi ne denir geçmez eğer? Ne komut bir şekilde çöker, ancak MySQL bağlantısı (bağlı havuzu falan) açık kalırsa? Ya da komut bir ölümcül kilitlenmeye girerse o olur?

Ben sadece had a problem bir betik oturumu açtı ve sonra (dosyayı barındırılan), diğer bilgisayar da aynı şeyi yapıyor iken, bir NFS paylaşımına üzerinde flock() bir dosyaya çalıştı nerede. Sonuç flock()-over-NFS çağrı her arama yaklaşık 30 saniye boyunca komut dosyası engelleme olmasıydı. Ve bu 20 yineleme bir döngü oldu! Harici bir operasyon olduğu için, PHP'nin komut dosyası zaman aşımı geçerli değildi ve oturumu 10 dakika içinde bu komut erişilen her zaman kilitli kaldım. Ve, şans bu ki gibi, bu bir AJAX shoutbox her 5 saniyede ... Binbaşı Showstopper tarafından boynuzsuz var senaryoydu.

Ben zaten daha iyi bir şekilde uygulamak için bazı fikirleri var, ama gerçekten diğer insanların önermek ne duymak istiyorum. Ben PHP ile çok deneyim ince kenar durumlarda bir gün her şeyi tehlikeye atabilecek gölgelerde tezgah bilmek o olmadı.


Added:

Tamam, kimse önermek için bir şey var gibi görünüyor. Tamam o zaman, burada benim fikrim. Ben bu yanlış gidebiliriz nereye bazı opinon istiyorum.

  1. InnoDB depolama motoru ile bir oturum tablo oluşturun. Bu bile kümelenmiş senaryolar altında bazı sıralarının uygun kilitlenmeyi sağlamak gerekir. Tablo sütunları olmalıdır ID, Data, LastAccessTime, LockTime, LockID. Onlar saklanan gereken verilerden oldukça doğrudan izleyin çünkü ben burada veritiplerini unutuyorum. ID PHP oturum kimliği olacaktır. Data tabii oturum verilerini içerecektir. LastAccessTime her okuma / yazma işlemi güncelleme olacak ve eski oturumları silmek için GC tarafından kullanılacak bir zaman damgası olacaktır. LockTime oturumu idi edinilen son kilidin bir zaman damgası olacak, ve LockID kilit bir GUID olacaktır.
  2. When a read operation is requested, there will be the following actions taken:
    1. Yürütme INSERT IGNORE INTO sessions (id, data, lastaccesstime, locktime, lockid) values ($sessid, null, now(), null, null); - Bu orada değilse oturum satır oluşturmak, ama zaten mevcut ise yapacak birşey yok;
    2. Değişken $ guid rastgele bir kilit kimliği oluşturmak;
    3. Bu (kilitli veya değilse kilit süresi dolmuş) oturum satırda bir kilit elde edecek ya da bir atomik işlem, ya da hiçbir şey yapacak - 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)); yürütmek.
    4. Kilit elde veya değilse mysql_affected_rows() ile kontrol edin. Bu alınmışsa - devam edin. Değilse - çalýştýrmayý her 0.5 saniyede bir yeniden çalışmayın. 40 saniye içinde kilidi hala elde değilse, bir istisna atar.
  3. Bir write işlemi istendiğinde, UPDATE sessions SET (lastaccesstime, data, locktime, lockid) values (now(), $data, null, null) where id=$sessid and lockid=$guid; Bu yeni veri ile oturum satır güncellemek ve hala kilit varsa kilidi kaldırabilirsiniz, fakat hiçbir şey yapacak başka bir atomik operasyon yürütmek Kilit zaten götürüldü eğer.
  4. Bir gc işleminin talep edildiğinde, sadece lastaccesstime çok eski olan tüm satırları silin.

Herkes bu kusurları görebilir miyim?

4 Cevap

Ok. The answer is going to be a bit longer - so patience! 1) Whatever I am going to write is based on the experiments I have done over last couple of days. There may be some knobs/settings/inner working I may not be aware of. If you spot mistakes/ or do not agree then please shout!

2) İlk açıklama - OTURUM veri okuma ve yazılı olduğu

Birden $ _SESSION Betiğinize içinde okur olsa bile oturum verileri tam olarak bir kere okumak olacak. Oturumdan okuma bir bir senaryo bazında olduğunu. Ayrıca veri session_id değil tuşlar dayalı umulur getir.

2) İkinci açıklama - HER ZAMAN SENARYO SONUNDA DİYE YAZMA

A) The write to session save_set_handler is always fired, even for scripts that only "read" from session and never do any writes. B) The write is only fired once, at the end of the script or if you explicitly call session_write_close. Again, the write is based on session_id and not keys

3) Üçüncü Açıklama: NEDEN BİZ kilitlemeyi İHTİYACINIZ

  • Bu yaygara her ne hakkında?
  • Biz gerçekten oturum kilitleri ihtiyacınız var mı?
  • Biz gerçekten bir Big Lock sarma OKU + yaz İhtiyacım Var

Yaygara açıklamak

Script1

  • 1: $ x = S_SESSION ["X"];
  • 2: sleep (20);
  • 3: if ($ x == 1) {
  • 4: / / bir şeyler yap
  • 5: $ _SESSION ["X"] = 3;
  • 6:}
  • 4: exit;

Script 2

  • 1: $ x = $ _SESSION ["X"];
  • 2: ($ x == 1) {$ _SESSION ["X"] = 2 ise; }
  • 3: exit;

Komut-1 zaten çalışırken başka bir komut dosyası tarafından değişti değeri: tutarsızlık bu komut 1 bir oturum değişkeni (3 hat) dayalı bir şey yapıyor olmasıdır. Bu bir iskelet örnek ama noktayı göstermektedir. Artık DOĞRU bir şey dayalı kararlar alıyor olması.

PHP varsayılan oturum kilitleme (Request Seviye kilitleme) kullanırken bu komut 1 kablosu1 okumaya başladım dosyadan okunamıyor çünkü script2 1. hatta engeller. Yani oturum verilerine istekleri tefrika edilir. Script2 bir değer okuduğunda, yeni değerini okumak için garanti edilir.

Açıklanması 4: PHP OTURUM SENKRONİZASYONU DEĞİŞKEN senkronizasyon FARKLI

Değişken bir senkronizasyon gibi sanki insanlar çok PHP oturum eşitleme hakkında konuşmak, kısa sürede herhangi bir komut değişken değeri ve sonraki okuma üzerine yazma gibi oluyor hafıza konumuna yazma yeni değeri almak olacaktır. Biz AÇIKLAMA # 1 gördüğünüz gibi - Bu doğru değil. Komut komut boyunca senaryonun başlangıcında okunan değerleri kullanır ve bazı diğer komut değerleri değişmiş olsa bile, çalışan betik sonraki refresh kadar yeni değerler hakkında bilemezsiniz. Bu çok important noktasıdır.

Ayrıca, hatta PHP büyük kilitleme ile oturum değişiklikleri değerlerin unutmayın. , Gibi şeyler "birinci oluyor komut değeri üzerine olacak" diyerek çok doğru değil. Değer değişimi ne peşinde yani, bu benim bilgim olmadan değiştirmek gerektiğini, tutarsızlık olduğunu, kötü değil.

AÇIKLAMA 5: GERÇEKTEN BÜYÜK KİLİT İHTİYACINIZ musunuz?

Şimdi, biz gerçekten Big Lock (istek düzeyi) ihtiyacım var? Cevap, DB izolasyon durumunda olduğu gibi, bu şeyleri yapmak istiyorum nasıl bağlıdır olmasıdır. $ _SESSION Varsayılan uygulama ile, IMHO, sadece büyük kilit mantıklı. Ben kullanımına benim betik boyunca başında okunan değer gidiyorum o zaman sadece büyük kilit mantıklı. Ben "her zaman", "taze" değerini almak için $ _SESSION uygulanmasını değiştirmek ise o zaman büyük kilit gerekmez.

Biz nesne sürüm gibi bir oturum veri sürüm düzeni uygulamak varsayalım. Komut-1 henüz noktasını yazmak gelmedi çünkü Şimdi, komut dosyası 2 yazma başarılı olacaktır. komut-2 oturum deposuna ve 1 ile sürümünü artırır script 1 çalışır oturumuna yazmak Şimdi zaman, bu (line: 5) başarısız olur -. yapılabilir olsa da, ben bu arzu olduğunu sanmıyorum.

===================================

Dan (1) ve (2), onu takip olursa olsun ne kadar karmaşık komut dosyası, X okur ve Y oturumu yazar ile,

  • oturum işleyicisi (okuma) ve yazma () yöntemleri yalnızca bir kez denir
  • ve her zaman denir

Şimdi, bazıları hala anlamaya çalışıyorum vb "değişken" düzeyinde kilitleme yapmaya çalışın net özel PHP oturum işlemcileri vardır. Ancak ben karmaşık şemaları lehine değilim.

$ _SESSION Ile PHP betikleri web sayfaları hizmet gerekiyordu ve Mili saniye işlenir olduğunu varsayarsak, ben ek karmaşıklık buna değer olduğunu sanmıyorum. Yazma hile yapmak gerekir sonra Like Peter Zaitsev mentions here ile güncelleme için bir seçme taahhüt.

İşte ben kilitlemeyi uygulamak için yazdığı kodu dahil. Bazı "Yarış simülasyonu" komut ile test etmek güzel olurdu. Ben işe gerektiğine inanıyoruz. Ben net üzerinde bulunan birçok doğru uygulamaları yoktur. Eğer hataları işaret edebilir eğer iyi olurdu. Ben çıplak mysqli'nin ile yaptım.

<?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);
            }

        }

    }
}
?>

Nesne oturum Handler kaydetmek için,

$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();

Burada PDO ile yapılan başka bir versiyonudur. Bu bir OturumKimliği varlığı için kontrol ve güncelleştirme veya Ekle yapar. Gereksiz yere her sayfa yük bir SQL sorgusu patlar gibi ben de () açık gelen gc işlevini kaldırdık. Bayat oturumu temizleme kolayca cron script ile yapılabilir. Bu PHP 5.x ise kullanmak sürümü olmalıdır Eğer herhangi bir hata bulursanız bana bildirin!

=========================================

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();
        }

    }
}
?>

Ben sadece eklemek istedim (ve zaten biliyor olabilir) (dosyalarını kullanır) PHP'nin varsayılan oturum saklama oturumları dosyaları kilitlemek gelmez. Açıkçası oturumları için dosyaları kullanarak muhtemelen neden bir veritabanı çözümü bakıyor olduğu eksiklikleri bol vardır.

Kilit elde veya değilse () mysql_affected_rows ile kontrol edin. Bu alınmışsa - devam edin. Değilse - çalýştýrmayý her 0.5 saniyede bir yeniden çalışmayın. 40 saniye içinde kilidi hala elde değilse, bir istisna atar.

Ben bir kilit için bu sürekli çek ile komut dosyası yürütme engelleme bir sorun bakın. Sen oturumun başlatılması bu kilit her şey arıyor kadar 40 saniye boyunca bu PHP çalıştırmak öneriyorsun (Ben doğru olduğunu okuyorum eğer.)

Recommendation

Kümelenmiş bir ortam varsa, ben son derece memcached öneriyoruz. O yüzden tüm kümelenmiş örneklerini memcached sunucuya erteleme olabilir bir sunucu / istemci ilişkiyi desteklemektedir. Bu korku konum konularını kilitleme ve bol hızlı bulunmamaktadır. Kendi sayfasından alıntı:

Ne olursa olsun (MS-SQL, Oracle, Postgres, MySQL-InnoDB, vb.) Kullanmak ne veritabanı, yükü bir sürü sorguları engellemek için gidiyoruz demektir, diskler söz konusu, özellikle, bir RDBMS ASİT özelliklerini uygulanmasında var . ASİT-uyumlu (MySQL MyISAM gibi değil) veritabanları için, bu yükü yok, ama okuma konuları yazma konuları üzerinde engeller. memcached never blocks.

Eğer hala bir RDBMS oturum deposuna kararlı (ve kilit bir sorun haline gelecektir endişeli) konum Aksi takdirde, size mimarisi hakkında başka hiçbir şey bilmek (burada çırpınışların.) Yapışkan bir oturum tanımlayıcı dayalı Sharding çeşit deneyebilirsiniz , ben alabilirim yaklaşık olarak belirli bulunuyor.

Benim soru hiç neden kilit? Neden sadece son yazma başarılı izin yok? Sen bir önbellek olarak oturum verilerini kullanarak olmamalıdır, bu yüzden yazar seyrek olma eğilimindedir, ve pratikte birbirlerini ezmek asla.