1. 程式人生 > >PHP高並發商城秒殺

PHP高並發商城秒殺

文件 mil 全部 鎖定 upd 樂觀 連接失敗 open 集中

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();

  

 壓力測試工具jmeter安裝方法 :    https://www.cnblogs.com/houss/p/10619453.html

PHP高並發商城秒殺