php商品秒杀超卖问题测试和redis解决学习

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

  1. 超发的原因

假设某个抢购场景中,我们一共只有50个产品,在最后一刻,我们已经消耗了49个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是1个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

2.一般秒杀测试
这里以TP5.1为例
在这里插入图片描述
按照一般思路,我们每卖出一件产品,都把数据库里的库存减去卖出的数目,在每次秒杀页面,执行抢购前先检查库存是否大于0。
使用 Apache自带的ab 测试工具我们模拟 并发60个用户的60个请求结果如下
在这里插入图片描述
我们再查看数据库的变化,库存已经变成了负数,并且订单生成了51个
在这里插入图片描述
在这里插入图片描述
3.redis解决思路
秒杀系统需要保证东西不多卖,关键是在多个客户端对库存进行减操作时,必须加锁。Redis中的Watch刚好可以实现一点。首先我们需要获取当前库存,只有库存中的食物小于购物车的数目才能对库存进行减。在高并发的情况下会出现某时刻查询库存够的,但下一时刻另外一个线程下单了,对库存进行减操作,刚好小于上个线程的购物车数目。照理现在的状态是不能下单成功的,因为库存已经不够了,但上一线程仍然认为数量还够,对库存进行减操作,从而导致库存出现负数的情况。
我们需要使用redis的原子操作来实现这个“单线程”。首先我们把库存存在goods_store:1这个列表中,假设有50件库存,就往列表中push50个数,这个数没有实际意义,仅仅只是代表一件库存。抢购开始后,每到来一个用户,就从goods_store:1中pop一个数,表示用户抢购成功。当列表为空时,表示已经被抢光了。因为列表的pop操作是原子的,即使有很多用户同时到达,也是依次执行的。

  1. 首先我们需要先安装redis,此处以windows+IIS为例,安装教程详见另一篇。

  2. 我们使用php做开发,安装php的redis扩展
    使用phpinfo()函数查看PHP的版本信息,这会决定扩展文件版本。

  3. 下载php_igbinary-1.2.1-5.5-ts-vc11-x64.zip,php_redis-2.2.5-5.6-ts-vc11-x64.zip(一定要保证版本的正确性)
    下载地址:
    ttp://windows.php.net/downloads/pecl/releases/redis/2.2.7/
    http://windows.php.net/downloads/pecl/releases/igbinary/1.2.1/

  4. 解压缩后,将php_redis.dll和php_igbinary.dll拷贝至php的ext目录下

  5. 修改php.ini,(PS:此php.ini文件是在Apache目录)在该文件中加入:
    ; php_redis
    extension=php_igbinary.dll
    extension=php_redis.dll
    注意:extension=php_igbinary.dll一定要放在extension=php_redis.dll的前面,否则此扩展不会生效

  6. 重启Apache后,使用phpinfo查看扩展是否成功安装

  7. 打开redis服务后,可以用如下测试是否能够调用。

<?php
  //连接本地的 Redis 服务
  $redis = new Redis();
 $redis->connect('127.0.0.1', 6379);
 echo "Connection to server sucessfully";
 //设置 redis 字符串数据
 $redis->set("tutorial-name", "Redis tutorial");
 // 获取存储的数据并输出
 echo "Stored string in redis:: " . $redis->get("tutorial-name");
?>

此处我们需要运用redis的列队以及pop操作的原子性,所以使用原生方法。
我们将秒杀分为三步,
秒杀前:用户不断刷新商品详情页,页面请求达到瞬时峰值。
秒杀开始:用户点击秒杀按钮,下单请求达到瞬时峰值。
秒杀后:一部分成功下单的用户不断刷新订单或者产生退单操作,大部分用户继续刷新商品详情页等待退单机会。

秒杀前: 将页面静态化,通过CDN缓存静态页面并通过浏览器缓存将用户点击页面请求流量拦截。
秒杀中: 使用redis中的队列,将提前存入的库存,每成功一次移除列表的头元素
秒杀后: 订单处理,数据库库存改变等操作

我们使用TP5来结合redis使用,在TP5中简单的封装了一个redis操作类,在/thinkphp/library/think/cache/driver/Redis.php中,这个类是供tp5中的cache使用的,只有几个简单的操作。在控制器中调用只需要

use think\Cache;
........
$vo = Db::name('cun')->order('id asc')->select();
Cache::set('listtest',$vo,0);

在此处我们使用redis原生操作

先运行http://域名/index.php/admin/Login/beforeredistest/id/1,将库存存入队列

//用户进入redistest页面抢购前先将库存redis
public function beforeredistest(){
	$id=input('id');
	$res=DB::name('product')->where('id','=',$id)->field('num')->find();	
	$redis = new \Redis;
    $redis->connect('127.0.0.1', 6379);
	$good_num_key="goods_".$id;//库存列队
	$len=$redis->llen($good_num_key);//获取当前redis库存,第一次执行此程序,当前redis库存为0
	$count=$res['num']-$len;
	for($i=0;$i<$count;$i++){
		$redis->lpush($good_num_key,1);//只能将一个值value插入到列表key的表头
	}
	echo $redis->llen($good_num_key);
}

运行结果
在这里插入图片描述

秒杀开始后

public function redistest(){
	//接入redis
	$redis = new \Redis;
    $redis->connect('127.0.0.1', 6379);
	$id=input('id');
	$good_num_key="goods_".$id;
	$n=$redis->lpop($good_num_key);//移除并返回列表key的头元素,返回[被删元素|false]
	if(!$n){//库存为0
		echo "木有啦";
	}else{
		$res=DB::name('product')->where('id','=',1)->field('num,title')->find();
		//先把库存减去n,再生成订单存入订单表,此处按照每单只能抢1件处理
		DB::name('product')->where('id','=',1)->setDec('num',1);
		$data['orderid']=date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
		$strs="QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm";
		$data['username']=substr(str_shuffle($strs),mt_rand(0,strlen($strs)-11),6);
		$data['create_time']=time();
		$data['productid']=$id;
		$data['productname']="多功能充电宝";
		DB::name('order')->insert($data);
		echo "抢到啦";
	}
	return $this->fetch();
}

我们使用 Apache自带的ab测试并发
在这里插入图片描述
查看数据库,并没有出现超卖的问题了(原库存为20)
在这里插入图片描述
在这里插入图片描述

总结
PHP文件的执行是单线程的,但是,服务器(apache/nigix/php-fpm)是多线程的。每次对某个PHP文件的访问服务器都会创建一个新的进程/线程,用来执行对应的PHP文件。
也就是说对于一个请求来说PHP是单线程的,但是多个请求间是并发的。
对应一个客户的一个页面请求处理的php 是单线程处理的, 这样一来就可以自上而下的去编辑/理解代码中的业务逻辑了, 但是 php 可以同时开很多线程来处理 很多用户请求的同一个PHP , 所以 php 也可以看成是"多线程"的。

在这里插入图片描述

上面的办法比较low,如果有一万件商品,那队列就有一万条

原理:后台创建商品后,将商品个数存入redis,采用常量键名拼接商品id 作为key,红包个数为value存入redis,然后当用户抢购商品的时候,前端传商品id到后端,后端 先加redis锁,然后根据接收的商品id 读取key对应的值就是商品剩余个数,然后减1后再存入。

$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));

if ($ok) {
    //获取到锁
    ... do something ...
    if ($redis->get($key) == $random) {
        $redis->del($key);
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值