本文主要和大家介紹了php session的鎖和並發,與之相關的現像有請求阻塞、session資料遺失、session資料讀不到的問題,有興趣的夥伴們可以參考一下,希望能幫忙到大家。
我登入不了了
某天,我準備登入我們一個後台系統,前去解決一個bug,在帳戶密碼驗證碼都準確輸入的情況下,我登入不上,經過多次實驗發現主要有兩個錯誤訊息:
csrf驗證失敗
#驗證碼錯誤【我對碼神起誓我用半角輸入了我看到的驗證碼,且順序一致,無多加字元】
我們的系統我們的系統是基於phalcon 2.0.8 開發的,如你所見,我們在表單域加入了防止csrf攻擊的域。也啟用了驗證碼。
<input type="hidden" name="{{ security.getTokenKey() }}" value="{{ security.getToken() }}"/> <img src="/login/getCaptcha" id="img-captcha"/>
我先對這兩個元件進行查閱,發現他們都是將資料存於session:
# phalcon/security.zep # Security::getToken() let session = <SessionInterface> dependencyInjector->getShared("session"); session->set(this->_tokenValueSessionID, token); $this->session->set('admin_get_captcha_action', $captcha);
然後我又查閱了我們session的實現,發現是將資料儲存於redis的。
找啊找什麼問題導致我登入不上呢?既然是資料驗證上出現問題,就從資料著手吧,我登陸我們測試環境的redis機器,執行redis-cli monitor,然後走一遍登入流程,發現輸出如下(意思):
GET sessionId
GET sessionId
##SETEX sessionId 3600 csrf=xxxx
SETEX sessionId 3600 captcha=abcd SETEX sessionId 3600 captcha=abcd
我們可以看到:
1、這裡有兩個請求,一次是表單加載,一次是產生驗證碼的。
2、存在「並發」的情況,這兩個請求應該是表單載入渲染後才請求驗證碼的,也就是session順序應該是get->set->get->set,看起來怎麼是並發請求了。
整個世界都不好了,不過也稍微明白是什麼問題了。什麼問題呢,說來話長,要從PHP的session資料的存取說起。 php的session資料的存取
session的資料是經過編碼成字串儲存在記憶體【file、db、redis、memcache等】的,在我們使用session的時候,是什麼時候去儲存器取資料的?又是什麼時候將資料寫入記憶體的?
這個問題的答案可能和一些朋友想的不一樣,一個請求裡面,PHP只會讀取一次存儲器,在session_start的時候,然後也只會寫入一次存儲器,在請求結束的時候,或呼叫session_write_close的時候,將資料刷回記憶體,關閉session。
###那麼問題來了:###1、如果一个会话,同时出现两个读写session请求,没有保证获取1-写入1-获取2-写入2,同时没有cas版本管理机制的情况下,这些并发请求就会彼此读取不到对方的写入,最后写入的会把前面请求写入的session覆盖掉。
2、如果请求是串行的,像登录页面的表单和验证码,也有可能前面的请求已经输出内容了,但是session还没写入,后面的请求就已经发起了。
锁与不锁
解决这种资源的并发一般会通过锁或版本管理来处理。但是版本管理我看不到好的方法。就聊聊锁吧。
其实锁是不大适合,有弊端的。
php的session,默认是用文件存储的,在打开session的时候,会对文件加独占锁,这样,其它请求就无法获取锁了,只能等待直到前面的锁解了。
这样保证了 读取-写入,读取-写入的顺序。
其它存储器,例如mysql,可以借助select for update进行行锁。redis可以通过一个自增键,返回1的获取到锁等来实现。
这个实现的话,对数据流来说很理想,但是,对于目前这种页面大量应用ajax的情况,所有请求排队处理,将大大加大页面展现的耗时,甚至出现请求超时等不可用故障。
没有解决的解决不建议过多使用session,其一次读取一次写入的机制所引发的问题,会造成坑的存在。
在模版渲染前,或请求输出前调用session_write_close
# 立刻回写session,避免session覆盖 $eventManager = $this->view->getEventsManager(); if (!$eventManager) { $eventManager = new Manager(); $this->view->setEventsManager($eventManager); } $eventManager->attach("view:afterRender",function(){ session_write_close(); }); return $this->view; if($login) { # 立刻回写session,避免session读取不到 $eventManager = $this->dispatcher->getEventsManager(); if (!$eventManager) { $eventManager = new Manager(); $this->dispatcher->setEventsManager($eventManager); } $eventManager->attach('dispatch:afterDispatchLoop',function(){ session_write_close(); }); return $this->response->setHeader('Location', '/'); }
相关推荐:
以上是PHP之session鎖定、並行、覆蓋詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!