併發請求導致的業務處理安全風險及解決方案

wyzsk發表於2020-08-19
作者: blue · 2013/12/25 11:51

0x00 背景


一段簡單的購買程式,看起來沒有任何問題。

剩餘餘額、商品庫存、購買許可權等判斷面面俱到,從頭到腳包裝的嚴嚴實實。

但是為何人一多就頻頻漏點吶?何解?

0x01 問題分析


還是以商城購買為例,商城網站是web程式和資料庫兩部分,業務處理流程:

#!shell
使用者金額是否大於商品價格—>商品庫存是否充足—>購買操作:生成訂單—>扣除使用者金額—>商品庫存減1

2013122510571541794.png

流程的每一部分都是web與資料庫打交道,查詢或者運算元據庫。

程式示例(PHP+MySQL)

#!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時,同時有多個請求,而當前沒有一個請求走到商品庫存減少位置,多次購買都能成功,而商城卻無貨可發。

2013122510574019546.png

總結來說,當有大量的購買操作同時進行,如果資料庫的處理速度跟不上程式的請求速度,就會出現判斷不準確的問題,造成使用者以單個商品的金額購買多個商品、某些使用者付款了但得不到商品等,算是一個安全風險。

0x02 解決方案:


核心思想:將一次業務處理流程(如購買操作)作為一個最小操作單元,同一時間只能有一個操作。

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('購買成功');
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章