php商城秒杀活动

今天在网上看到一篇思路+代码的商城秒杀实例,我觉得非常不错,借鉴一下分享给大家:

一、前言

  双十一刚过不久,大家都知道在天猫、京东、苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价1999现在秒杀价只要999的手机时,会迎来一个用户请求的高峰期,可能会有几十万几百万的并发量,来抢这个手机,在高并发的情形下会对数据库服务器或者是文件服务器应用服务器造成巨大的压力,严重时说不定就宕机了,另一个问题是,秒杀的东西都是有量的,例如一款手机只有10台的量秒杀,那么,在高并发的情况下,成千上万条数据更新数据库(例如10台的量被人抢一台就会在数据集某些记录下 减1),那次这个时候的先后顺序是很乱的,很容易出现10台的量,抢到的人就不止10个这种严重的问题。那么,以后所说的问题我们该如何去解决呢? 接下来我所分享的技术就可以拿来处理以上的问题: 分布式锁。

二、实现思路

  1.Redis实现分布式锁思路

    思路很简单,主要用到的redis函数是setnx(),这个应该是实现分布式锁最主要的函数。首先是将某一任务标识名(这里用Lock:order作为标识名的例子)作为键存到redis里,并为其设个过期时间,如果是还有Lock:order请求过来,先是通过setnx()看看是否能将Lock:order插入到redis里,可以的话就返回true,不可以就返回false。当然,在我的代码里会比这个思路复杂一些,我会在分析代码时进一步说明。

  2.Redis实现任务队列

    这里的实现会用到上面的Redis分布式的锁机制,主要是用到了Redis里的有序集合这一数据结构。例如入队时,通过zset的add()函数进行入队,而出队时,可以用到zset的getScore()函数。另外还可以弹出顶部的几个任务。

  以上就是实现 分布式锁 和 任务队列 的简单思路,如果你看完有点模棱两可,那请看接下来的代码实现。

三、代码分析

  (一)先来分析Redis分布式锁的代码实现  

    (1)为避免特殊原因导致锁无法释放,在加锁成功后,锁会被赋予一个生存时间(通过lock方法的参数设置或者使用默认值),超出生存时间锁会被自动释放锁的生存时间默认比较短(秒级),因此,若需要长时间加锁,可以通过expire方法延长锁的生存时间为适当时间,比如在循环内。

    (2)系统级的锁当进程无论何种原因时出现crash时,操作系统会自己回收锁,所以不会出现资源丢失,但分布式锁不用,若一次性设置很长时间,一旦由于各种原因出现进程crash 或者其他异常导致unlock未被调用时,则该锁在剩下的时间就会变成垃圾锁,导致其他进程或者进程重启后无法进入加锁区域。

    先看加锁的实现代码:这里需要主要两个参数,一个是$timeout,这个是循环获取锁的等待时间,在这个时间内会一直尝试获取锁知道超时,如果为0,则表示获取锁失败后直接返回而不再等待;另一个重要参数的$expire,这个参数指当前锁的最大生存时间,以秒为单位的,它必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放。这个参数的最要作用请看上面的(1)里的解释。

    这里先取得当前时间,然后再获取到锁失败时的等待超时的时刻(是个时间戳),再获取到锁的最大生存时刻是多少。这里redis的key用这种格式:"Lock:锁的标识名",这里就开始进入循环了,先是插入数据到redis里,使用setnx()函数,这函数的意思是,如果该键不存在则插入数据,将最大生存时刻作为值存储,假如插入成功,则对该键进行失效时间的设置,并将该键放在$lockedName数组里,返回true,也就是上锁成功;如果该键存在,则不会插入操作了,这里有一步严谨的操作,那就是取得当前键的剩余时间,假如这个时间小于0,表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用,这时可以直接设置expire并把锁纳为己用。如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出循环,反之则 隔 $waitIntervalUs 后继续 请求。 这就是加锁的整一个代码分析。

 1 <?php
 2 /**
 3 * 加锁
 4 * @param  [type]  $name           锁的标识名
 5 * @param  integer $timeout        循环获取锁的等待超时时间,在此时间内会一直尝试获取锁直到超时,为0表示失败后直接返回不等待
 6 * @param  integer $expire         当前锁的最大生存时间(秒),必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放
 7 * @param  integer $waitIntervalUs 获取锁失败后挂起再试的时间间隔(微秒)
 8 * @return [type]                  [description]
 9 */
10 public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) {
11     if ($name == null) return false;
12      
13     //取得当前时间
14     $now = time();
15     //获取锁失败时的等待超时时刻
16     $timeoutAt = $now + $timeout;
17     //锁的最大生存时刻
18     $expireAt = $now + $expire;
19     $redisKey = "Lock:{$name}";
20     while (true) {
21         //将rediskey的最大生存时刻存到redis里,过了这个时刻该锁会被自动释放
22         $result = $this->redisString->setnx($redisKey, $expireAt);
23         if ($result != false) {
24             //设置key的失效时间
25             $this->redisString->expire($redisKey, $expireAt);
26             //将锁标志放到lockedNames数组里
27             $this->lockedNames[$name] = $expireAt;
28             return true;
29         }
30         //以秒为单位,返回给定key的剩余生存时间
31         $ttl = $this->redisString->ttl($redisKey);
32         //ttl小于0 表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)
33         //如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用
34         //这时可以直接设置expire并把锁纳为己用
35         if ($ttl < 0) {
36             $this->redisString->set($redisKey, $expireAt);
37             $this->lockedNames[$name] = $expireAt;
38             return true;
39         }
40         /*****循环请求锁部分*****/
41         //如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出
42         if ($timeout <= 0 || $timeoutAt < microtime(true)) break;
43         //隔 $waitIntervalUs 后继续 请求
44         usleep($waitIntervalUs);
45     }
46     return false;
47 }

接着看解锁的代码分析:解锁就简单多了,传入参数就是锁标识,先是判断是否存在该锁,存在的话,就从redis里面通过deleteKey()函数删除掉锁标识即可。

 1 <?php
 2 /**
 3 * 解锁
 4 * @param  [type] $name [description]
 5 * @return [type]       [description]
 6 */
 7 public function unlock($name) {
 8     //先判断是否存在此锁
 9     if ($this->isLocking($name)) {
10         //删除锁
11         if ($this->redisString->deleteKey("Lock:$name")) {
12             //清掉lockedNames里的锁标志
13             unset($this->lockedNames[$name]);
14             return true;
15         }
16     }
17     return false;
18 }
19    

再贴上删除掉所有锁的方法,其实都一个样,多了个循环遍历而已。

 1 <?php
 2 /**
 3 * 释放当前所有获得的锁
 4 * @return [type] [description]
 5 */
 6 public function unlockAll() {
 7     //此标志是用来标志是否释放所有锁成功
 8     $allSuccess = true;
 9     foreach ($this->lockedNames as $name => $expireAt) {
10     if (false === $this->unlock($name)) {
11     $allSuccess = false;
12     }
13     }
14     return $allSuccess;
15 }

以上就是用Redis实现分布式锁的整一套思路和代码实现的总结和分享,这里我附上正一个实现类的代码,代码里我基本上对每一行进行了注释,方便大家快速看懂并且能模拟应用。想要深入了解的请看整个类的代码:

  1 <?php
  2 /**
  3 *在redis上实现分布式锁
  4 */
  5 class RedisLock {
  6     private $redisString;
  7     private $lockedNames = [];
  8     public function __construct($param = NULL) {
  9         $this->redisString = RedisFactory::get($param)->string;
 10     }
 11      
 12     /**
 13     * 加锁
 14     * @param  [type]  $name           锁的标识名
 15     * @param  integer $timeout        循环获取锁的等待超时时间,在此时间内会一直尝试获取锁直到超时,为0表示失败后直接返回不等待
 16     * @param  integer $expire         当前锁的最大生存时间(秒),必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放
 17     * @param  integer $waitIntervalUs 获取锁失败后挂起再试的时间间隔(微秒)
 18     * @return [type]                  [description]
 19     */
 20     public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) {
 21         if ($name == null) return false;
 22          
 23         //取得当前时间
 24         $now = time();
 25         //获取锁失败时的等待超时时刻
 26         $timeoutAt = $now + $timeout;
 27         //锁的最大生存时刻
 28         $expireAt = $now + $expire;
 29          
 30         $redisKey = "Lock:{$name}";
 31         while (true) {
 32             //将rediskey的最大生存时刻存到redis里,过了这个时刻该锁会被自动释放
 33             $result = $this->redisString->setnx($redisKey, $expireAt);
 34              
 35             if ($result != false) {
 36                 //设置key的失效时间
 37                 $this->redisString->expire($redisKey, $expireAt);
 38                 //将锁标志放到lockedNames数组里
 39                 $this->lockedNames[$name] = $expireAt;
 40                 return true;
 41             }
 42              
 43             //以秒为单位,返回给定key的剩余生存时间
 44             $ttl = $this->redisString->ttl($redisKey);
 45              
 46             //ttl小于0 表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)
 47             //如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用
 48             //这时可以直接设置expire并把锁纳为己用
 49             if ($ttl < 0) {
 50                 $this->redisString->set($redisKey, $expireAt);
 51                 $this->lockedNames[$name] = $expireAt;
 52                 return true;
 53             }
 54              
 55             /*****循环请求锁部分*****/
 56             //如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出
 57             if ($timeout <= 0 || $timeoutAt < microtime(true)) break;
 58              
 59             //隔 $waitIntervalUs 后继续 请求
 60             usleep($waitIntervalUs);
 61          
 62         }
 63          
 64         return false;
 65     }
 66     /**
 67     * 解锁
 68     * @param  [type] $name [description]
 69     * @return [type]       [description]
 70     */
 71     public function unlock($name) {
 72         //先判断是否存在此锁
 73         if ($this->isLocking($name)) {
 74             //删除锁
 75             if ($this->redisString->deleteKey("Lock:$name")) {
 76                 //清掉lockedNames里的锁标志
 77                 unset($this->lockedNames[$name]);
 78                 return true;
 79             }
 80         }
 81         return false;
 82     }
 83     /**
 84     * 释放当前所有获得的锁
 85     * @return [type] [description]
 86     */
 87     public function unlockAll() {
 88         //此标志是用来标志是否释放所有锁成功
 89         $allSuccess = true;
 90         foreach ($this->lockedNames as $name => $expireAt) {
 91             if (false === $this->unlock($name)) {
 92                 $allSuccess = false;
 93             }
 94         }
 95         return $allSuccess;
 96     }
 97     /**
 98     * 给当前所增加指定生存时间,必须大于0
 99     * @param  [type] $name [description]
100     * @return [type]       [description]
101     */
102     public function expire($name, $expire) {
103         //先判断是否存在该锁
104         if ($this->isLocking($name)) {
105             //所指定的生存时间必须大于0
106             $expire = max($expire, 1);
107             //增加锁生存时间
108             if ($this->redisString->expire("Lock:$name", $expire)) {
109                 return true;
110             }
111         }
112         return false;
113     }
114     /**
115     * 判断当前是否拥有指定名字的所
116     * @param  [type]  $name [description]
117     * @return boolean       [description]
118     */
119     public function isLocking($name) {
120         //先看lonkedName[$name]是否存在该锁标志名
121         if (isset($this->lockedNames[$name])) {
122             //从redis返回该锁的生存时间
123             return (string)$this->lockedNames[$name] = (string)$this->redisString->get("Lock:$name");
124         }
125          
126         return false;
127     }
128 }

文章来源:https://blog.youkuaiyun.com/lihe460186709/article/details/53182995  感谢博主分享

转载于:https://www.cnblogs.com/phpxj/p/10082527.html

### PHP 实现前后端交互支持秒杀活动 #### 后端逻辑构建 为了确保高并发环境下的稳定性,后端采用 Redis 来管理库存状态并减少数据库压力。初始化 Redis 客户端连接: ```php require 'vendor/autoload.php'; $redis = new Predis\Client([ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379, ]); ``` 每次请求到来时,先检查商品是否有足够的库存量。如果存在,则尝试执行原子减法操作以扣减库存[^1]。 ```php function trySeckill($productId){ global $redis; // 获取当前剩余库存数 $stockKey = "seckill:product:$productId"; $remainingStock = (int)$redis->get($stockKey); if ($remainingStock <= 0) { return false; // 库存不足 } // 尝试原子性地减少库存 $result = $redis->decr($stockKey); if ((int)$result >= 0) { // 成功购买 return true; } else { // 取消本次操作并将库存恢复原状 $redis->incr($stockKey); return false; } } ``` #### 前端交互设计 前端部分主要负责向服务器发送异步请求以及更新界面显示。这里可以利用 JavaScript 的 `fetch` API 或者 jQuery 发送 AJAX 请求给后端接口,并根据返回的结果调整页面内容[^4]。 ```javascript async function attemptPurchase(productId) { const response = await fetch(`/api/seckill/${productId}`, { method: 'POST'}); const result = await response.json(); if(result.success === true){ alert('恭喜您抢购成功!'); }else{ alert('很遗憾,未能抢到该商品'); } } // 绑定点击事件至按钮上 document.getElementById('buy-now').addEventListener('click', () => { let productId = document.querySelector('#product-id').value; attemptPurchase(productId); }); ``` 对于倒计时功能而言,在 HTML 中定义好时间容器标签之后,可以通过定时器不断刷新距离目标时刻还剩多少秒,并实时渲染到界面上去。 ```html <div id="countdown"></div> <script type="text/javascript"> let endTime = Date.parse(new Date("{{ end_time }}")) / 1000; function updateCountDown() { var nowTime = Math.floor(Date.now()/1000), diffSecs = endTime - nowTime; if(diffSecs > 0){ document.getElementById('countdown').innerText = `${Math.floor(diffSecs/(60*60))}:${String(Math.floor((diffSecs%3600)/60)).padStart(2,'0')}:${String(diffSecs%60).padStart(2,'0')}`; setTimeout(updateCountDown, 1000); } else { document.getElementById('countdown').innerText = "秒杀已结束!"; } } updateCountDown(); </script> ``` #### Web Server 配置建议 考虑到性能因素,推荐使用 Nginx 结合 FastCGI 和 php-fpm 处理动态脚本解析工作流[^3]。这不仅能够提高响应速度,还能更好地应对大量并发访问需求。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值