抢购、秒杀是平常很常见的场景,面试的时候面试官也经常会问到,比如问你淘宝中的抢购秒杀是怎么实现的等等。
抢购、秒杀实现很简单,但是有些问题需要解决,主要针对两个问题:
一、高并发对数据库产生的压力
二、竞争状态下如何解决库存的正确减少("超卖"问题)
第一个问题,对于PHP来说很简单,用缓存技术就可以缓解数据库压力,比如memcache,redis等缓存技术。
第二个问题就比较复杂点:
常规写法:
查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大于0处,如果在高并发下就会有问题,导致库存量出现负数(超卖)。
例如
(1)A购买商品,查询库存为1,可以购买
(2)减少库存,提交订单
(1)和(2)之间执行是有时间的,这中间B也来购买这个商品,他查询出的库存也是1,之后提交订单,库存就会变为-1
例如
(1)A购买商品,查询库存为1,可以购买
(2)B购买商品,查询库存为1,可以购买(出现脏读)
(3)A提交订单,减少库存,库存减至0
(4)B提交订单,减少库存,库存减至-1
<?php
$conn = mysql_connect("localhost","root","root");
if(!$conn)
{
echo "数据库连接失败";
exit;
}
mysql_select_db("test",$conn);
$price = 10;
$user_id = 1;
$goods_id = 1;
$sku_id = 11;
$number = 1;
//生成唯一订单号
function build_order_no()
{
return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function add_num_log($event,$type=0)
{
global $conn;
$sql = "insert into ih_log(event,type)
values('$event','$type')";
mysql_query($sql,$conn);
}
//模拟下单操作
//库存是否大于0
$sql = "select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
$rs = mysql_query($sql,$conn);
$row = mysql_fetch_assoc($rs);
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 = mysql_query($sql,$conn);
//库存减少
$sql = "update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs = mysql_query($sql,$conn);
if(mysql_affected_rows())
{
add_num_log('库存减少成功');
}
else
{
add_num_log('库存减少失败');
}
}
else
{
exit('库存不够');
}
下面是redis的乐观锁思路
例如
(1)A购买商品,获取库存为2(版本1)
(2)B购买商品,获取库存为2(版本1)
(3)A提交订单,减少库存,比较版本,一致则执行,库存减至1(更新后的版本为2)
(4)B提交订单,减少库存,比较版本,不一致会阻止执行
<?php
$redis = new redis();
$redis->connect('127.0.0.1',6379);
$redis->watch('number');//在提交时检测会不会被多人修改,版本是否一致
//从redis中获得库存
$number = $redis->get('number');
if($number <= 0)
{
exit('库存不足');
}
$redis->multi();//标记一个事务块的开始
$redis->decr('number');//redis内库存减少
$result = $redis->exec();//结束,检测版本号,不一致会阻止执行
if($result)
(
//mysql操作
)
至于为什么不使用悲观锁和队列
1、不使用悲观锁,因为等待时间非常长,响应慢
2、不使用队列,因为并发量会让队列内存瞬间升高
以上就是php+redis实现秒杀的思路,实际的业务处理会更复杂,酌情参考。