標籤:blog http io os 使用 ar java strong 檔案
有一個答題的小項目,表的欄位如下:
id 使用者id
times 答題次數
questions 回答的問題id,這是一個php serialize()的字串
當使用者答完題像後端提交結果時,構建的post包如下:
{ ‘id‘:1, ‘questionid‘:38}
後台邏輯如下:
$user = User::findFirst("id = ?1",array(‘id‘=>$_POST[‘id‘]));//取得使用者模型$questions = unserialize($user->questions);//還原序列化$questionid = (int)$_POST[‘questionid‘];//題目idif ( ! in_array($questionid,$questions)) {//判斷使用者是否答過,沒有答過則將題目id添加到questions欄位裡 $questions[] = $questionid; $user->times += 1;//統計答題次數 $user->questions = serialize($questions);//序列化questions $user->save();//update} else { exit(‘對不起,這個題目你已經答過‘);}
看上去流程沒有問題啊,使用者每次提交的時候會先讀取自己的資料,檢測是否答過這個題目,答過則直接退出。
但是出問題了:
當活動推出的第二天,後台資料發現有個使用者的times答題次數和questions記錄數不匹配,表現為 times=120 count(unserialize(questions))=17。
當時很奇怪,代碼正常流程是沒問題的,卡了半天,一直沒想出來使用者如何造成這種資料的,直到剛才,終於開竅,正如題目中所說,問題出在並發上,這個使用者可能自己寫指令碼或者藉助第三方工具,類比post請求,並使用多線程等技術造成並發請求,我們來類比一下並發過程:
使用者同時發起兩個請求,php也就分配了兩個進程來處理,當著兩個進程幾乎同時執行了這行代碼:
$user = User::findFirst("id = ?1",array(‘id‘=>$_POST[‘id‘]));//取得使用者模型
此時在兩個進程中的$user資料是一樣的,我們假設一下,此時$user->questions 的值為 {1,2}。
假設兩個請求構建的post資料包分別為:
{ ‘id‘:1, ‘questionid‘:3}
和
{ ‘id‘:1, ‘questionid‘:4}
那麼當代碼執行到:
in_array($questionid,$questions)
的時候實際上返回的都是 false
他們相繼發起了 $user->save() ,分別希望將 3 和 4 加進questions,注意,這時候questions在資料庫中的值還是{1,2},而sql語句是一條條執行的,也就是說資料庫按順序執行了兩次update操作,先是將questions的值更改為 {1,2,3},隨即第二個save()執行又將其改為了{1,2,4},兩次times都增加1,所以最終的結果時:
times questions
4 {1,2,4}
這時候如果使用者繼續提交 ‘questionid‘:3 ,則又變更為:
times questions
5 {1,2,3,4}
這是兩個並發的情況,會造成times比questions的數量多 1
如果類比10個並發呢?不想算了。
問題既然已經出現了,還是得解決啊,首先想到用innodb的行級鎖,很遺憾,表是myisam…
最終的解決方案是通過作業系統檔案獨佔鎖定實現的,螢幕前的朋友如果有興趣可以看:
http://my.oschina.net/cxz001/blog/281130
php當資料庫update遇到並發,一個小坑