高并發(fā)下的數(shù)據(jù)安全
我們知道在多線(xiàn)程寫(xiě)入同一個(gè)文件的時(shí)候,會(huì)出現(xiàn)“線(xiàn)程安全”的問(wèn)題(多個(gè)線(xiàn)程同時(shí)運(yùn)行同一段代碼,如果每次運(yùn)行結(jié)果和單線(xiàn)程運(yùn)行的結(jié)果是一樣的,結(jié)果和預(yù)期相同,就是線(xiàn)程安全的)。
如果是MySQL數(shù)據(jù)庫(kù),可以使用它自帶的鎖機(jī)制很好的解決問(wèn)題,但是在大規(guī)模并發(fā)的場(chǎng)景中,是不推薦使用MySQL的。秒殺和搶購(gòu)的場(chǎng)景中,最關(guān)鍵的問(wèn)題,就是“超發(fā)”,如果在這方面控制不慎,會(huì)導(dǎo)致實(shí)際產(chǎn)生的訂單比預(yù)售商品還多的問(wèn)題。
超發(fā)的原因:(推薦學(xué)習(xí):PHP編程從入門(mén)到精通)
假設(shè)某個(gè)搶購(gòu)場(chǎng)景中,我們一共只有100個(gè)商品,在最后一刻,我們已經(jīng)消耗了99個(gè)商品,僅剩最后一個(gè)。這個(gè)時(shí)候,系統(tǒng)發(fā)來(lái)多個(gè)并發(fā)請(qǐng)求,這批請(qǐng)求讀取到的商品余量都是1個(gè),然后都通過(guò)了余量的判斷,最終導(dǎo)致超發(fā)。
值得注意的是:記得將庫(kù)存字段number字段設(shè)為unsigned,當(dāng)庫(kù)存為0時(shí),因?yàn)閡nsigned字段不能為負(fù)數(shù),將會(huì)返回false
優(yōu)化方案
優(yōu)化1:使用MySQL的事務(wù),鎖住操作的行 BEGIN ; SELECT … FOR UPDATE ; COMMIT ; ROLLBACK
<?php //優(yōu)化方案1:使用MySQL的事務(wù),鎖住操作的行 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"); //開(kāi)始事務(wù) $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此時(shí)這條記錄被鎖住,其它事務(wù)必須等待此次事務(wù)提交后才能執(zhí)行 $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); //庫(kù)存減少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs=mysqli_query($conn,$sql); if($store_rs){ echo '庫(kù)存減少成功'; insertLog('庫(kù)存減少成功'); mysqli_query($conn,"COMMIT");//事務(wù)提交即解鎖 }else{ echo '庫(kù)存減少失敗'; insertLog('庫(kù)存減少失敗'); } }else{ echo '庫(kù)存不夠'; insertLog('庫(kù)存不夠'); mysqli_query($conn,"ROLLBACK"); }
優(yōu)化2:文件鎖的思路
對(duì)于日訪(fǎng)問(wèn)量不高或者說(shuō)并發(fā)數(shù)不是很大的應(yīng)用,用一般的文件操作方法完全沒(méi)有問(wèn)題。但如果并發(fā)高,在我們對(duì)文件進(jìn)行讀寫(xiě)操作時(shí),很有可能多個(gè)進(jìn)程對(duì)進(jìn)一文件進(jìn)行操作,如果這時(shí)不對(duì)文件的訪(fǎng)問(wèn)進(jìn)行相應(yīng)的獨(dú)占,就容易造成數(shù)據(jù)丟失。
<?php //優(yōu)化方案2:使用非阻塞的文件排他鎖 include ('./mysql.php'); //生成唯一訂單號(hào) 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 "系統(tǒng)繁忙,請(qǐng)稍后再試"; 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){//庫(kù)存是否大于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); //庫(kù)存減少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs = mysqli_query($conn,$sql); if($store_rs){ echo '庫(kù)存減少成功'; insertLog('庫(kù)存減少成功'); flock($fp,LOCK_UN);//釋放鎖 }else{ echo '庫(kù)存減少失敗'; insertLog('庫(kù)存減少失敗'); } }else{ echo '庫(kù)存不夠'; insertLog('庫(kù)存不夠'); } fclose($fp);