高并發下的數據安全
我們知道在多線程寫入同一個文件的時候,會出現“線程安全”的問題(多個線程同時運行同一段代碼,如果每次運行結果和單線程運行的結果是一樣的,結果和預期相同,就是線程安全的)。
如果是MySQL數據庫,可以使用它自帶的鎖機制很好的解決問題,但是在大規模并發的場景中,是不推薦使用MySQL的。秒殺和搶購的場景中,最關鍵的問題,就是“超發”,如果在這方面控制不慎,會導致實際產生的訂單比預售商品還多的問題。
超發的原因:(推薦學習:PHP編程從入門到精通)
假設某個搶購場景中,我們一共只有100個商品,在最后一刻,我們已經消耗了99個商品,僅剩最后一個。這個時候,系統發來多個并發請求,這批請求讀取到的商品余量都是1個,然后都通過了余量的判斷,最終導致超發。
值得注意的是:記得將庫存字段number字段設為unsigned,當庫存為0時,因為unsigned字段不能為負數,將會返回false
優化方案
優化1:使用MySQL的事務,鎖住操作的行 BEGIN ; SELECT … FOR UPDATE ; COMMIT ; ROLLBACK
<?php //優化方案1:使用MySQL的事務,鎖住操作的行 include('./mysql.php'); function build_order_no(){ return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8); } //記錄日志 function insertLog($event,$type=0){ global $conn; $sql="insert into ih_log(event,type) values('$event','$type')"; mysqli_query($conn,$sql); } mysqli_query($conn,"BEGIN"); //開始事務 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此時這條記錄被鎖住,其它事務必須等待此次事務提交后才能執行 $rs=mysqli_query($conn,$sql); $row=$rs->fetch_assoc(); if($row['number']>0){ //生成訂單 $order_sn=build_order_no(); $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price) values('$order_sn','$user_id','$goods_id','$sku_id','$price')"; $order_rs=mysqli_query($conn,$sql); //庫存減少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs=mysqli_query($conn,$sql); if($store_rs){ echo '庫存減少成功'; insertLog('庫存減少成功'); mysqli_query($conn,"COMMIT");//事務提交即解鎖 }else{ echo '庫存減少失敗'; insertLog('庫存減少失敗'); } }else{ echo '庫存不夠'; insertLog('庫存不夠'); mysqli_query($conn,"ROLLBACK"); }
優化2:文件鎖的思路
對于日訪問量不高或者說并發數不是很大的應用,用一般的文件操作方法完全沒有問題。但如果并發高,在我們對文件進行讀寫操作時,很有可能多個進程對進一文件進行操作,如果這時不對文件的訪問進行相應的獨占,就容易造成數據丟失。
<?php //優化方案2:使用非阻塞的文件排他鎖 include ('./mysql.php'); //生成唯一訂單號 function build_order_no(){ return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8); } //記錄日志 function insertLog($event,$type=0){ global $conn; $sql="insert into ih_log(event,type) values('$event','$type')"; mysqli_query($conn,$sql); } $fp = fopen("lock.txt", "w+"); if(!flock($fp,LOCK_EX | LOCK_NB)){ echo "系統繁忙,請稍后再試"; return; } //下單 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'"; $rs = mysqli_query($conn,$sql); $row = $rs->fetch_assoc(); if($row['number']>0){//庫存是否大于0 //模擬下單操作 $order_sn=build_order_no(); $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price) values('$order_sn','$user_id','$goods_id','$sku_id','$price')"; $order_rs = mysqli_query($conn,$sql); //庫存減少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs = mysqli_query($conn,$sql); if($store_rs){ echo '庫存減少成功'; insertLog('庫存減少成功'); flock($fp,LOCK_UN);//釋放鎖 }else{ echo '庫存減少失敗'; insertLog('庫存減少失敗'); } }else{ echo '庫存不夠'; insertLog('庫存不夠'); } fclose($fp);