ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
**什么是秒杀?** 秒杀会产生一个瞬间的高并发,使用数据库会增加数据库的访问压力,也会降低访问速度,所以我们应该使用缓存,来降低数据库的访问压力;可以看出这里的操作和原来的下单是不一样的:产生的秒杀预订单不会马上写入数据库,会先写入缓存,等用户支付成功时,修改状态,写入数据库。 **场景描述** 假设num是存储在数据库中的字段,保存了被秒杀产品的剩余数量。 ~~~text if($num > 0){ //用户抢购成功,记录用户信息 $num--; } ~~~ 假设在一个并发量较高的场景,数据库中 num 的值为 1 时,可能同时会有多个进程读取到 num 为 1,程序判断符合条件,抢购成功,num 减一。这样会导致商品超发的情况,本来只有 10 件可以抢购的商品,可能会有超过 10 个人抢到,此时 num 在抢购完成之后为负值。 **解决方案** 解决该问题的方案由很多,可以简单分为基于 mysql 和 redis 的解决方案,redis 的性能要由于 mysql,因此可以承载更高的并发量,不过下面介绍的方案都是基于单台 mysql 和 redis 的,更高的并发量需要分布式的解决方案 **基于 redis 的解决方案** 1、基于 watch 的乐观锁方案 watch 用于监视一个 (或多个) key ,如果在事务执行之前这个 (或这些) key 被其他命令所改动,那么事务将被打断。这种方案跟 mysql 中的乐观锁方案类似,具体表现也是一样的。 ~~~text $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"; } ~~~ 2、基于 list 的队列方案 基于队列的方案利用了 redis 出队操作的原子性,抢购开始之前首先将商品编号放入响应的队列中,在抢购时依次从队列中弹出操作,这样可以保证每个商品只能被一个进程获取并操作,不存在超发的情况。该方案的优点是理解和实现起来都比较简单,缺点是当商品数量较多是,需要将大量的数据存入到队列中,并且不同的商品需要存入到不同的消息队列中。 ~~~text public function init(){ $this->redis->del('goods'); for($i=1;$i<=10;$i++){ $this->redis->lPush('goods',$i); } $this->redis->del('result'); echo 'init done'; } public function run(){ $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; } } } ~~~ 3、基于 decr 返回值的方案 如果我们将剩余量 num 设置为一个键值类型,每次先 get 之后判断,然后再 decr 是不能解决超发问题的。但是 redis 中的 decr 操作会返回执行后的结果,可以解决超发问题。我们首先 get 到 num 的值进行第一步判断,避免每次都去更新 num 的值,然后再对 num 执行 decr 操作,并判断 decr 的返回值,如果返回值不小于 0,这说明 decr 之前是大于 0 的,用户抢购成功。 ~~~text public function run(){ $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"; } } ~~~ 4、基于 setnx 的排它锁方案 redis 没有像 mysql 中的排它锁,但是可以通过一些方式实现排它锁的功能,就类似 php 使用文件锁实现排它锁一样。 setnx 实现了 exists 和 set 两个指令的功能,若给定的 key 已存在,则 setnx 不做任何动作,返回 0;若 key 不存在,则执行类似 set 的操作,返回 1。我们设置一个超时时间 timeout,每隔一定时间尝试 setnx 操作,如果设置成功就是获得了相应的锁,执行 num 的 decr 操作,操作完成删除相应的 key,模拟释放锁的操作。 ~~~text public function run(){ 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"); } } ~~~