page contents

PHP高并发商城秒杀

秒杀活动是一些购物平台推出的集中人气的活动,一般商品数量很少,价格很便宜,限定开始购买的时间,会在以秒为单位的时间内被购买一空。

attachments-2020-09-rDiqNnFL5f5098b1dfa3b.png


1.什么是秒杀

秒杀活动是一些购物平台推出的集中人气的活动,一般商品数量很少,价格很便宜,限定开始购买的时间,会在以秒为单位的时间内被购买一空。比如原价千元甚至万元的商品以一元的价格出售,但数量只有一件,在某天的某个时间开始出售,这就造成很多人去抢这一件商品。


2.秒杀会带来的问题

(1)高并发

比较火热的秒杀在线人数都是10w起的,如此之高的在线人数对于网站架构从前到后都是一种考验。

(2)超卖

任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难题。


3.解决的方式

前台方面:

A:扩容

加机器,这是最简单的方法,通过增加前端池的整体承载量来抗峰值。

B:静态化

将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。

C:限流

一般都会采用IP级别的限流,即针对某一个IP,限制单位时间内发起请求数量。或者活动入口的时候增加游戏或者问题环节进行消峰操作。


后台方面:

A: 锁机制

乐观锁,就是在数据库设计一个版本号的字段,每次修改都使其+1,这样在提交时比对提交前的版本号就知道是不是并发提交了,但是有个缺点就是只能是应用中控制,如果有跨应用修改同一条数据乐观锁就没办法了,这个时候可以考虑悲观锁。

悲观锁,就是直接在数据库层面将数据锁死,类似于oralce中使用select xxxxx from xxxx where xx=xx for update,这样其他线程将无法提交数据。

文件锁,通过锁定文件来判断,确保其他线程无法提交数据

B: redis队列

引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。

优点:解决超卖问题,略微提升性能。

缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。


4.演示

原生写法,会出现多卖现象

<?php
header("content-type:text/html;charset=utf-8");
 
$dsn='mysql:host=localhost;dbname=test';
try {
    $conn= new PDO($dsn, 'root', 'root');
    $conn->exec("set names utf8");
 
} catch (PDOException $e) {
    exit('数据库连接失败,错误信息:'. $e->getMessage());
}
 
$price    = 10;
$user_id  = 1;
$goods_id = 1;
$sku_id   = 11;
$number   = 1;
 
//生成唯一订单
function build_order_no(){
    return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
 
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
$rs  = $conn->query($sql);
$row = $rs->fetch(); //获取一行数据
 
if ($row['number']>0) {//库存是否大于0
    $order_sn=build_order_no();
    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
    $conn->query($sql);
 
    //库存减少
    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
    $f = $conn->query($sql);
    if ($f) {
        // 库存减少成功
    } else {
        // 库存减少失败
    }
} else {
    // 库存不够
}
echo "success";

 通过文件锁来解决

<?php
header("content-type:text/html;charset=utf-8");
 
$dsn='mysql:host=localhost;dbname=test';
try {
    $conn= new PDO($dsn, 'root', 'root');
    $conn->exec("set names utf8");
 
} catch (PDOException $e) {
    exit('数据库连接失败,错误信息:'. $e->getMessage());
}
 
$price    = 10;
$user_id  = 1;
$goods_id = 1;
$sku_id   = 11;
$number   = 1;
 
//生成唯一订单
function build_order_no(){
    return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";//解锁 此时ih_store数据中goods_id='$goods_id' and sku_id='$sku_id' 的数据被锁住(注3),其它事务必须等待此次事务 提交后才能执行
$rs  = $conn->query($sql);
$row = $rs->fetch(); //获取一行数据
 
$fp = fopen("lock.txt", "w+"); // 通过文件锁住操作执行完再执行下一个
if (!flock($fp, LOCK_EX | LOCK_NB)) {
    echo "系统繁忙,请稍后再试";
    return;
}
 
if ($row['number']>0) {//库存是否大于0
    $order_sn=build_order_no();
    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
    $conn->query($sql);
 
    //库存减少
    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
    $f = $conn->query($sql);
    if ($f) {
        // insertLog('库存减少成功');
        flock($fp, LOCK_UN);//释放锁
    } else {
        // insertLog('库存减少失败');
    }
} else {
    // insertLog('库存不够');
}
fclose($fp);
echo "success";

通过redis队列来实现

  客户端

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis_name = 'miaosha';
$sales='goods_store';
//
$uid = mt_rand(1000,9999);
$store = 10;
usleep(100000);// usleep()函数的功能是把调用该函数的线程挂起一段时间 [1]  , 单位是微秒(microseconds:即百万分之一秒)
if ($store > $redis->get($sales)) {
    $redis->incr($sales);
    $redis->lpush($redis_name, $uid);//
    // echo $uid."秒杀成功|";
} else {
    // echo "秒杀已结束";
}
 
$redis->close();

服务端

<?php
header("content-type:text/html;charset=utf-8");
 
$dsn='mysql:host=localhost;dbname=test';
try {
    $conn= new PDO($dsn, 'root', 'root');
    $conn->exec("set names utf8");
 
} catch (PDOException $e) {
    exit('数据库连接失败,错误信息:'. $e->getMessage());
}
 
$price    = 10;
$user_id  = 1;
$goods_id = 1;
$sku_id   = 11;
$number   = 1;
 
//生成唯一订单
function build_order_no(){
    return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
 
$redis  = new Redis();
$result = $redis->connect('127.0.0.1',6379);
//通过死循环从队列中一直取值
while (true) {
    //模拟下单操作
    //下单前判断redis队列库存量
    $userid  = $redis->rpop('miaosha');
 
    if(!$userid){
        // insertLog('error:no store redis');
        continue;
    }
    //生成订单
    $order_sn = build_order_no();
    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
    values('$order_sn','$userid','$goods_id','$sku_id','$price')";
    $conn->query($sql);
 
    //库存减少
    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
    $f = $conn->query($sql);
    if ($f) {
        // insertLog('库存减少成功');
    }else{
        // insertLog('库存减少失败');
    }
}
$redis->close();


attachments-2020-09-p8Ll9ZxW5f5098c9131d8.jpg

  • 发表于 2020-09-03 15:19
  • 阅读 ( 648 )
  • 分类:高并发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1474 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章