高并发超卖问题:测试出现超卖问题和解决方案
说明:当前测试为thinkphp5环境下的代码、不考虑用户uid问题,只考虑库存问题
准备:
1. 新建两个表(goods、orders)
CREATE TABLE `goods` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL DEFAULT '', `number` int NOT NULL DEFAULT '0', `price` int NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=gbk CREATE TABLE `order` ( `id` int NOT NULL AUTO_INCREMENT, `uid` int NOT NULL DEFAULT '0', `goods_id` int NOT NULL DEFAULT '0', `number` int NOT NULL DEFAULT '0', `add_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=gbk
2.插入两条商品数据
insert into `seo`.`ctx_goods` ( `name`, `number`, `price`) values ( '苹果手机', '10', '1'); insert into `seo`.`ctx_goods` ( `name`, `number`, `price`) values ( '苹果电脑', '3', '2');
3.购买商品业务代码
public function order(){ $goods_id = 2; $number = 1; // 启动事务 Db::startTrans(); try{ $goods = Db::name('goods')->lock(true)->find($goods_id); if($goods['number']>0){ $res1 = Db::name('goods')->where(['id' => $goods_id])->setDec('number', $number); $orderData = [ 'goods_id' => $goods['id'], 'uid' => 1, 'number' => $number, ]; $res2 = Db::name('order')->insert($orderData); if($res1 !==false && $res2){ // 提交事务 Db::commit(); exit('ok'); }else{ throw new \Exception(' 下单失败'); } }else{ throw new \Exception(' 库存不足'); } } catch (\Exception $e) { // 回滚事务 Db::rollback(); exit('error'); } exit; }
4. 测试产生超卖问题
注意:ab测试工具如果没有安装请百度搜索ab工具安装
ab -c500 -n800 -k http://seo_ctx.cn/portal/index/order
5. 结果
商品表数据
订单表数据
出现问题原因:
高并发,导致mysql查询同出来的数据一样,而在同一时间修改没有立即生效,之后这些修改都会被执行。
从而出现,库存只有3个却生成了3个以上的订单
解决方案:
1.设置数据库行锁
使用行锁,限制同一时间只能进行一个数据修改操作
将 $goods = Db::name('goods')->find($goods_id); 改成 $goods = Db::name('goods')->lock(true)->find($goods_id);
2.使用redis
用redis去维护库存
- redis是基于内存的,内存的读写速度非常快;
- redis是单线程的,省去了很多上下文切换线程的时间;
public function order(){ $goods_id = 2; $number = 1; $goods = Db::name('goods')->find($goods_id); $redis = new \Redis(); $redis->connect('127.0.0.1', 6379); if($redis->get('goodsNumber:'.$goods_id)!==false){ //设置初始库存 $redis->set('goodsNumber:'.$goods_id, $goods['number']); } // 启动事务 Db::startTrans(); try{ $remain_number = $redis->get('goodsNumber:'.$goods_id); if($remain_number>0){ $redis->decr('goodsNumber:'.$goods_id, 1); $res1 = Db::name('goods')->where(['id' => $goods_id])->setDec('number', $number); $orderData = [ 'goods_id' => $goods_id, 'uid' => 1, 'number' => $number, ]; $res2 = Db::name('order')->insert($orderData); if($res1 !==false && $res2){ // 提交事务 Db::commit(); exit('ok'); }else{ throw new \Exception(' 下单失败'); } }else{ throw new \Exception(' 库存不足'); } } catch (\Exception $e) { // 回滚事务 Db::rollback(); exit('error'); } exit; }
小绵羊