一段簡單的購買程序,看起來沒有任何問題。
剩余余額、商品庫存、購買權限等判斷面面俱到,從頭到腳包裝的嚴嚴實實。
但是為何人一多就頻頻漏點吶?何解?
還是以商城購買為例,商城網站是web程序和數據庫兩部分,業務處理流程:
#!shell
用戶金額是否大于商品價格—>商品庫存是否充足—>購買操作:生成訂單—>扣除用戶金額—>商品庫存減1
流程的每一部分都是web與數據庫打交道,查詢或者操作數據庫。
#!php
$goods=$db->FirstRow("SELECT * FROM ".Tb('goods')." WHERE goods_id='{$goods_id}'");
if(empty($goods)) ShowError('商品不存在');
/* 金額是否充足 */
if($user->money<$goods['price']) ShowError('金額不足,請充值');
/* 商品庫存 */
if($goods['num']==0) ShowError('庫存不足');
/* 購買操作 begin */
//生成訂單
CreateOrder($goods,$user,time());
$user->Update('money'=>$user->money-$goods['price']); //用戶金額減少
$db->Execute("UPDATE ".Tb('goods')." SET num=num-1 WHERE goods_id='{$goods_id}'");//商品庫存-1
ShowSuccess('購買成功');
/* 購買操作 end */
正常來看這個業務處理是沒有問題的,下面想象下多人同時購買(并發請求,如秒殺活動)的情境可能會引發的問題?
如果一個用戶同時有兩次購買請求,一次購買已進行到添加訂單但未扣除用戶金額,另一次購買在第一步用戶金額判斷便不準確了。
當商品庫存僅為1時,同時有多個請求,而當前沒有一個請求走到商品庫存減少位置,多次購買都能成功,而商城卻無貨可發。
總結來說,當有大量的購買操作同時進行,如果數據庫的處理速度跟不上程序的請求速度,就會出現判斷不準確的問題,造成用戶以單個商品的金額購買多個商品、某些用戶付款了但得不到商品等,算是一個安全風險。
核心思想:將一次業務處理流程(如購買操作)作為一個最小操作單元,同一時間只能有一個操作。
1. 整個操作加內存鎖。如在memcache里,開始購買時設置購買狀態為進行中,購買結束后清除購買狀態,程序開始時即從memcache里判斷是否有正在進行的購買操作,如有則退出。
2. 限制每個用戶的購買間隔,如10秒內僅允許購買一次,最好也是放在內存里。
3. 當然,優化數據庫及程序以加快處理速度也是有必要的。
解決方案程序示例(PHP+MySQL+Memcached)
#!php
/**
* 通過memcache解決并發購買問題
*/
$goods=$db->FirstRow("SELECT * FROM ".Tb('goods')." WHERE goods_id='{$goods_id}'");
if(empty($goods)) ShowError('商品不存在');
$mmc=memcache_init();
$lastBuyTime=$mmc->get('lastBuyTime_'.$user->userId);
if($lastBuyTime>0 && $lastBuyTime>time()-10) ShowError('10秒內只能進行一次購買');
$buying=$mmc->get('buying');
if($buying==1) ShowError('有正在進行的購買,請稍候');
/* 金額是否充足 */
if($user->money<$goods['price']) ShowError('金額不足,請充值');
/* 商品庫存 */
if($goods['num']==0) ShowError('庫存不足');
/* 購買操作 begin */
//生成訂單
CreateOrder($goods,$user,time());
$user->Update('money'=>$user->money-$goods['price']); //用戶金額減少
$db->Execute("UPDATE ".Tb('goods')." SET num=num-1 WHERE goods_id='{$goods_id}'");//商品庫存-1
/* 購買操作 end */
$mmc->set('buying',0);
$mmc->set('lastBuyTime_'.$user->userId,time());
ShowSuccess('購買成功');