怎么解决PHP高并发(商品秒杀)问题?两种解决方案分享

怎么解决PHP高并发(商品秒杀)问题?下面本篇文章就来给大家分享两种解决方案(基于mysql或基于Redis),希望对大家有所帮助。 秒杀会产生一个瞬间的高并发,使用数据库会增加数...

怎么解决PHP高并发商品秒杀)问题?下面本篇文章就来给大家分享两种解决方案(基于mysql或基于Redis),希望对大家有所帮助。

怎么解决PHP高并发(商品秒杀)问题?两种解决方案分享插图1

秒杀会产生一个瞬间的高并发,使用数据库会增加数据库的访问压力,也会降低访问速度,所以我们应该使用缓存,来降低数据库的访问压力;

可以看出这里的操作和原来的下单是不一样的:产生的秒杀预订单不会马上写入数据库,会先写入缓存,等用户支付成功时,修改状态,写入数据库。

假设num是存储在数据库中的字段,保存了被秒杀产品的剩余数量。

if($num>0){//用户抢购成功,记录用户信息$num--;}

假设在一个并发量较高的场景,数据库中num的值为1时,可能同时会有多个进程读取到num为1,程序判断符合条件,抢购成功,num减一。

这样会导致商品超发的情况,本来只有10件可以抢购的商品,可能会有超过10个人抢到,此时num在抢购完成之后为负值。

解决该问题的方案由很多,可以简单分为基于mysql和redis的解决方案,redis的性能要由于mysql,因此可以承载更高的并发量,不过下面介绍的方案都是基于单台mysql和redis的,更高的并发量需要分布式的解决方案,本文没有涉及。

一、基于mysql的解决方案

商品表 goods

CREATETABLE`goods`(`id`int(11)NOTNULL,`num`int(11)DEFAULTNULL,`version`int(11)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8

抢购结果表 log

CREATETABLE`log`(`id`int(11)NOTNULLAUTO_INCREMENT,`good_id`int(11)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8

①悲观锁

悲观锁的方案采用的是排他读,也就是同时只能有一个进程读取到num的值。事务在提交或回滚之后,锁会释放,其他的进程才能读取。

该方案最简单易懂,在对性能要求不高时,可以直接采用该方案。要注意的是,SELECT … FOR UPDATE要尽可能的使用索引,以便锁定尽可能少的行数;

排他锁是在事务执行结束之后才释放的,不是读取完成之后就释放,因此使用的事务应该尽可能的早些提交或回滚,以便早些释放排它锁。

$this->mysqli->begin_transaction();$result=$this->mysqli->query("SELECTnumFROMgoodsWHEREid=1LIMIT1FORUPDATE");$row=$result->fetch_assoc();$num=intval($row['num']);if($num>0){usleep(100);$this->mysqli->query("UPDATEgoodsSETnum=num-1");$affected_rows=$this->mysqli->affected_rows;if($affected_rows==1){$this->mysqli->query("INSERTINTOlog(good_id)VALUES({$num})");$affected_rows=$this->mysqli->affected_rows;if($affected_rows==1){$this->mysqli->commit();echo"success:".$num;}else{$this->mysqli->rollback();echo"fail1:".$num;}}else{$this->mysqli->rollback();echo"fail2:".$num;}}else{$this->mysqli->commit();echo"fail3:".$num;}

②乐观锁

乐观锁的方案在读取数据是并没有加排他锁,而是通过一个每次更新都会自增的version字段来解决,多个进程读取到相同num,然后都能更新成功的问题。在每个进程读取num的同时,也读取version的值,并且在更新num的同时也更新version,并在更新时加上对version的等值判断。

假设有10个进程都读取到了num的值为1,version值为9,则这10个进程执行的更新语句都是

UPDATEgoodsSETnum=num-1,version=version+1WHEREversion=9

然而当其中一个进程执行成功之后,数据库中version的值就会变为10,剩余的9个进程都不会执行成功,这样保证了商品不会超发,num的值不会小于0,但这也导致了一个问题,那就是发出抢购请求较早的用户可能抢不到,反而被后来的请求抢到了。

$result=$this->mysqli->query("SELECTnum,versionFROMgoodsWHEREid=1LIMIT1");$row=$result->fetch_assoc();$num=intval($row['num']);$version=intval($row['version']);if($num>0){usleep(100);$this->mysqli->begin_transaction();$this->mysqli->query("UPDATEgoodsSETnum=num-1,version=version+1WHEREversion={$version}");$affected_rows=$this->mysqli->affected_rows;if($affected_rows==1){$this->mysqli->query("INSERTINTOlog(good_id)VALUES({$num})");$affected_rows=$this->mysqli->affected_rows;if($affected_rows==1){$this->mysqli->commit();echo"success:".$num;}else{$this->mysqli->rollback();echo"fail1:".$num;}}else{$this->mysqli->rollback();echo"fail2:".$num;}}else{echo"fail3:".$num;}

③where条件(原子操作)

悲观锁的方案保证了数据库中num的值在同一时间只能被一个进程读取并处理,也就是并发的读取进程到这里要排队依次执行。

乐观锁的方案虽然num的值可以被多个进程同时读取到,但是更新操作中version的等值判断可以保证并发的更新操作在同一时间只能有一个更新成功。

还有一种更简单的方案,只在更新操作时加上num>0的条件限制即可。通过where条件限制的方案虽然看似和乐观锁方案类似,都能够防止超发问题的出现,但在num较大时的表现还是有很大区别的。

假如此时num为10,同时有5个进程读取到了num=10,对于乐观锁的方案由于version字段的等值判断,这5个进程只会有一个更新成功,这5个进程执行完成之后num为9;

对于where条件判断的方案,只要num>0都能够更新成功,这5个进程执行完成之后num为5。

$result=$this->mysqli->query("SELECTnumFROMgoodsWHEREid=1LIMIT1");$row=$result->fetch_assoc();$num=intval($row['num']);if($num>0){usleep(100);$this->mysqli->begin_transaction();$this->mysqli->query("UPDATEgoodsSETnum=num-1WHEREnum>0");$affected_rows=$this->mysqli->affected_rows;if($affected_rows==1){$this->mysqli->query("INSERTINTOlog(good_id)VALUES({$num})");$affected_rows=$this->mysqli->affected_rows;if($affected_rows==1){$this->mysqli->commit();echo"success:".$num;}else{$this->mysqli->rollback();echo"fail1:".$num;}}else{$this->mysqli->rollback();echo"fail2:".$num;}}else{echo"fail3:".$num;}

二、基于redis的解决方案

①基于watch的乐观锁方案

watch用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

这种方案跟mysql中的乐观锁方案类似,具体表现也是一样的。

$num=$this->redis->get('num');if($num>0){$this->redis->watch('num');usleep(100);$res=$this->redis->multi()->decr('num')->lPush('result',$num)->exec();if($res==false){echo"fail1";}else{echo"success:".$num;}}else{echo"fail2";}

②基于list的队列方案

基于队列的方案利用了redis出队操作的原子性,抢购开始之前首先将商品编号放入响应的队列中,在抢购时依次从队列中弹出操作,这样可以保证每个商品只能被一个进程获取并操作,不存在超发的情况。

该方案的优点是理解和实现起来都比较简单,缺点是当商品数量较多是,需要将大量的数据存入到队列中,并且不同的商品需要存入到不同的消息队列中。

publicfunctioninit(){$this->redis->del('goods');for($i=1;$i<=10;$i++){$this->redis->lPush('goods',$i);}$this->redis->del('result');echo'initdone';}publicfunctionrun(){$goods_id=$this->redis->rPop('goods');usleep(100);if($goods_id==false){echo"fail1";}else{$res=$this->redis->lPush('result',$goods_id);if($res==false){echo"writelog:".$goods_id;}else{echo"success".$goods_id;}}}

③基于decr返回值的方案

如果我们将剩余量num设置为一个键值类型,每次先get之后判断,然后再decr是不能解决超发问题的。

但是redis中的decr操作会返回执行后的结果,可以解决超发问题。我们首先get到num的值进行第一步判断,避免每次都去更新num的值,然后再对num执行decr操作,并判断decr的返回值,如果返回值不小于0,这说明decr之前是大于0的,用户抢购成功。

publicfunctionrun(){$num=$this->redis->get('num');if($num>0){usleep(100);$retNum=$this->redis->decr('num');if($retNum>=0){$res=$this->redis->lPush('result',$retNum);if($res==false){echo"writeLog:".$retNum;}else{echo"success:".$retNum;}}else{echo"fail1";}}else{echo"fail2";}}

④基于setnx的排它锁方案

redis没有像mysql中的排它锁,但是可以通过一些方式实现排它锁的功能,就类似php使用文件锁实现排它锁一样。

setnx实现了exists和set两个指令的功能,若给定的key已存在,则setnx不做任何动作,返回0;若key不存在,则执行类似set的操作,返回1。

我们设置一个超时时间timeout,每隔一定时间尝试setnx操作,如果设置成功就是获得了相应的锁,执行num的decr操作,操作完成删除相应的key,模拟释放锁的操作。

publicfunctionrun(){do{$res=$this->redis->setnx("numKey",1);$this->timeout-=100;usleep(100);}while($res==0&&$this->timeout>0);if($res==0){echo'fail1';}else{$num=$this->redis->get('num');if($num>0){$this->redis->decr('num');usleep(100);$res=$this->redis->lPush('result',$num);if($res==false){echo"fail2";}else{echo"success:".$num;}}else{echo"fail3";}$this->redis->del("numKey");}}

产品猿社区致力收录更多优质的商业产品,给服务商以及软件采购客户提供更多优质的软件产品,帮助开发者变现来实现多方共赢;

日常运营的过程中我们难免会遇到各种版权纠纷等问题,如果您在社区内发现有您的产品未经您授权而被用户提供下载或使用,您可按照我们投诉流程处理,点我投诉

本文来自用户发布投稿,不代表产品猿立场 ;若对此文有疑问或内容有严重错误,可联系平台客服反馈;

部分产品是用户投稿,可能本文没有提供官方下下载地址或教程,若您看到的内容没有下载入口,您可以在我们产品园商城搜索看开发者是否有发布商品;若您是开发者,也诚邀您入驻商城平台发布的产品,地址:点我进入

如若转载,请注明出处:https://www.chanpinyuan.cn/33868.html;
(0)
上一篇 2023年1月21日 下午4:17
下一篇 2023年1月21日 下午4:17

相关推荐

发表回复

登录后才能评论
分享本页
返回顶部