# 并发请求导致的业务处理安全风险及解决方案

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2013-12-25T03:51:00Z
Canonical: https://wepostx.com/topics/286

> 乌云历史资料归档
>
> **原始作者：** blue
> **原始编号：** superkieran-wooyundrops:123
> **原始发布时间：** 2013-12-25 11:51
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

## 0x00 背景

一段简单的购买程序，看起来没有任何问题。
剩余余额、商品库存、购买权限等判断面面俱到，从头到脚包装的严严实实。
但是为何人一多就频频漏点呐？何解？

## 0x01 问题分析

还是以商城购买为例，商城网站是web程序和数据库两部分，业务处理流程：

```text
#!shell 用户金额是否大于商品价格—>商品库存是否充足—>购买操作:生成订单—>扣除用户金额—>商品库存减1
```

![原文图片](/media/2016/06/6631ff7306bee799c3c969b260bc7964)

流程的每一部分都是web与数据库打交道，查询或者操作数据库。

### 程序示例(PHP+MySQL)

```text
#!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时，同时有多个请求，而当前没有一个请求走到商品库存减少位置，多次购买都能成功，而商城却无货可发。

![原文图片](/media/2016/06/0b42bc6457cdddbbe80e06f8a10b476a)

总结来说，当有大量的购买操作同时进行，如果数据库的处理速度跟不上程序的请求速度，就会出现判断不准确的问题，造成用户以单个商品的金额购买多个商品、某些用户付款了但得不到商品等，算是一个安全风险。

## 0x02 解决方案：

核心思想：将一次业务处理流程（如购买操作）作为一个最小操作单元，同一时间只能有一个操作。

```text
1. 整个操作加内存锁。如在memcache里，开始购买时设置购买状态为进行中，购买结束后清除购买状态，程序开始时即从memcache里判断是否有正在进行的购买操作，如有则退出。 2. 限制每个用户的购买间隔，如10秒内仅允许购买一次，最好也是放在内存里。 3. 当然，优化数据库及程序以加快处理速度也是有必要的。
```
解决方案程序示例(PHP+MySQL+Memcached)

```text
#!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('购买成功');
```

## Replies
