《高并發(fā)秒殺搶購(gòu)系統(tǒng)設(shè)計(jì)》PHP示例代碼

一年多以前在學(xué)校分享過(guò)一次《高并發(fā)秒殺搶購(gòu)系統(tǒng)設(shè)計(jì)》桨武,其中有部分示例代碼未能貼出呀酸,因?yàn)楫?dāng)時(shí)工作換電腦導(dǎo)致程序代碼丟失琼梆,一直就沒(méi)有貼出來(lái),到編寫(xiě)本文時(shí)有不少朋友向我要過(guò)代碼错览,很不好意思一直沒(méi)整理就沒(méi)給倾哺,近期有時(shí)間就整理了一下羞海。時(shí)間有點(diǎn)久了,一些內(nèi)容細(xì)節(jié)有些忘記却邓,示例代碼處理模型如有考慮不到之處申尤,請(qǐng)留言給我昧穿,我會(huì)跟進(jìn)測(cè)試修改,提前謝謝各位时鸵。

沒(méi)有看過(guò)上一篇文章的饰潜,可以先看看一次分享《高并發(fā)秒殺搶購(gòu)系統(tǒng)設(shè)計(jì)》彭雾。

本次整理代碼所用的相關(guān)程序版本:

  • PHP5.6加pthreads、redis半沽、mysql擴(kuò)展
  • Mysql5.7 吴菠,不過(guò)用不到數(shù)據(jù)庫(kù)的高級(jí)特性做葵,5.0及以上版本支持Innodb存儲(chǔ)引擎的就可以
  • Redis5.0.5
  • Centos7.8

嘗試了PHP7.3和7.4的多線程,無(wú)論是pthreads還是parallel都出現(xiàn)“段錯(cuò)誤”無(wú)法正常執(zhí)行榨乎,可能和Centos環(huán)境有些關(guān)系棠涮,有能執(zhí)行成功的朋友請(qǐng)指教一下严肪。

準(zhǔn)備工作,建庫(kù)建表驳糯,test庫(kù)就行,建表語(yǔ)句:

create table goods (
    id int unsigned not null auto_increment primary key,
    goodname varchar(50) not null default '',
    total int not null default 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

insert into goods(goodname,total) values('火車票',100);

新手錯(cuò)誤代碼

先看新手最容易犯錯(cuò)的代碼恬偷,它的處理邏輯在單進(jìn)程單線程沒(méi)有并發(fā)的情況下是對(duì)的袍患,但是在高并發(fā)下就是錯(cuò)誤的竣付。

error_reporting(E_ALL ^ E_DEPRECATED);
class Conf {
    public static $host = 'localhost';
    public static $port = '3306';
    public static $user = 'root';
    public static $passwd = '123';
    public static $dbname = 'test';
}
class NoLock extends Thread {
    public function run() {
        //模擬真實(shí)環(huán)境肆良,連接數(shù)據(jù)庫(kù)筛璧,每次都返回一個(gè)新的數(shù)據(jù)庫(kù)連接
        $mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
        mysql_select_db(Conf::$dbname);
        //從數(shù)據(jù)庫(kù)中取出庫(kù)存
        $sql = "select total from goods where id=1";
        $result = mysql_query($sql,$mysql);
        $info = mysql_fetch_assoc($result);
        mysql_free_result($result);
        //獲取庫(kù)存余量
        $total = $info['total'];
        echo 'tid='.self::getCurrentThreadId().' total='.$total."\n";
        if($total > 0) {//判斷庫(kù)存是否還有
            /*
             * 這里會(huì)出現(xiàn)兩種寫(xiě)法,但是結(jié)果都一樣巫糙,都是錯(cuò)誤的
             * 一種是直接數(shù)據(jù)庫(kù)字段減1
             * 另一種是取出的庫(kù)存數(shù)減1再寫(xiě)回?cái)?shù)據(jù)庫(kù)
             */
//            mysql_query("update goods set total=total-1 where id=1");
            mysql_query("update goods set total='".($total-1)."' where id=1",$mysql);
        }
        mysql_close($mysql);
    }
}

$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
$sql = "update goods set total=100 where id=1";
mysql_query($sql,$mysql);
mysql_close($mysql);

$clientArr = [];
for ($i=0;$i<100;++$i) {
    $clientArr[$i] = new NoLock();
    $clientArr[$i]->start();
}
//獲取結(jié)果
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
$sql = "select total from goods where id=1";
$result = mysql_query($sql,$mysql);
$info = mysql_fetch_assoc($result);
mysql_free_result($result);
mysql_close($mysql);
echo 'end total='.$info['total']."\n";

Nolock類繼承PHP線程類Thread参淹,一個(gè)線程模擬一個(gè)用戶下單減庫(kù)存承二,100個(gè)庫(kù)存需要100個(gè)線程亥鸠,按正常邏輯100個(gè)線程執(zhí)行完畢庫(kù)存是0就對(duì)负蚊。上面這段代碼可直接復(fù)制到一個(gè)PHP文件颓哮,修改頂部的Mysql配置冕茅,然后多次執(zhí)行(一定要多次快速執(zhí)行)姨伤,你能夠發(fā)現(xiàn)好多時(shí)候最后庫(kù)存大于0,有的線程讀取到了相同的庫(kù)存当编。
分析一下:100個(gè)庫(kù)存徒溪,100個(gè)用戶都已經(jīng)完成下單,還有剩余鲤桥,繼續(xù)執(zhí)行的話一定是要超賣(mài)了~~~

悲觀鎖芜壁,利用Mysql實(shí)現(xiàn)

error_reporting(E_ALL ^ E_DEPRECATED);
class Conf {
    public static $host = 'localhost';
    public static $port = '3306';
    public static $user = 'root';
    public static $passwd = '';
    public static $dbname = 'test';
}
//利用mysql數(shù)據(jù)庫(kù)實(shí)現(xiàn)悲觀鎖
class PessimisticLock extends Thread {
    public function run() {
        //模擬真實(shí)環(huán)境,連接數(shù)據(jù)庫(kù)顷牌,每次都返回一個(gè)新的數(shù)據(jù)庫(kù)連接
        $mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
        mysql_select_db(Conf::$dbname);
        //從數(shù)據(jù)庫(kù)中取出庫(kù)存 利用Innodb更新行鎖實(shí)現(xiàn)悲觀鎖
        $sql = "update goods set total=total-1 where id=1 and total>0";
        $result = mysql_query($sql,$mysql);
        //要檢查修改影響的條數(shù)塞淹,執(zhí)行成功但不一定修改數(shù)據(jù)
        $affectedRows = $result ? mysql_affected_rows() : 0;
        if($affectedRows) {//根據(jù)修改影響的條數(shù)進(jìn)行后續(xù)操作
            echo self::getCurrentThreadId()." update ok \n";
        } else {
            echo self::getCurrentThreadId()." update err \n";
        }
        mysql_close($mysql);
    }
}

$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
$sql = "update goods set total=100 where id=1";
mysql_query($sql,$mysql);
mysql_close($mysql);

$clientArr = [];
for ($i=0;$i<100;++$i) {
    $clientArr[$i] = new PessimisticLock();
    $clientArr[$i]->start();
}

//獲取結(jié)果
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
$sql = "select total from goods where id=1";
$result = mysql_query($sql,$mysql);
$info = mysql_fetch_assoc($result);
mysql_free_result($result);
mysql_close($mysql);
echo 'end total='.$info['total']."\n";

這段代碼多次使勁執(zhí)行运挫,最后庫(kù)存都是0谁帕,所以這個(gè)方法原理上可以冯袍,也只是原理上可以,不建議直接用在高并發(fā)系統(tǒng)上儡循,主要因?yàn)樗鼤?huì)大幅度增加數(shù)據(jù)庫(kù)負(fù)載择膝。我們對(duì)系統(tǒng)優(yōu)化一般首先著手的都是減少數(shù)據(jù)庫(kù)的直接操作肴捉,因此這個(gè)方法不建議,真要用還需要看具體情況呵扛。

樂(lè)觀鎖每庆,利用Redis的事務(wù)來(lái)實(shí)現(xiàn)

error_reporting(E_ALL ^ E_DEPRECATED);
class Conf {
    public static $host = 'localhost';
}

//利用redis事務(wù)實(shí)現(xiàn)樂(lè)觀鎖
class OptimisticLock extends Thread {
    public function run() {
        $redis = new Redis();
        $redis->connect(Conf::$host);
        do {//只要還有庫(kù)存且沒(méi)成功減庫(kù)存就一直執(zhí)行
            $goodsTotal = $redis->get('goods_total');
            echo self::getCurrentThreadId().' total='.$goodsTotal."\n";
            if($goodsTotal <= 0) break;//每次都檢查是否還有庫(kù)存  沒(méi)有庫(kù)存退出循環(huán)
            $redis->watch('goods_total');
            $redis->multi();
            $redis->decr('goods_total');
            $res = $redis->exec();
        } while(!$res);
    }
}
//初始化緩存庫(kù)存數(shù)據(jù)
$redis = new Redis();
$redis->connect(Conf::$host);
$redis->set('goods_total',100);

$clientArr = [];
for ($i=0;$i<100;++$i) {
    $clientArr[$i] = new OptimisticLock();
    $clientArr[$i]->start();
}

這段代碼使勁多次執(zhí)行,最后庫(kù)存也是0今穿,所以也是可行的缤灵,這個(gè)方法也是首先推薦的方法,內(nèi)存中的數(shù)據(jù)操作比在數(shù)據(jù)庫(kù)中要快得多蓝晒,負(fù)載能力會(huì)高跟多腮出。
本分享給出的示例代碼只是處理邏輯,具體應(yīng)用還要根據(jù)具體服務(wù)器架構(gòu)甚至是業(yè)務(wù)邏輯進(jìn)行調(diào)整芝薇。有不足之處歡迎批評(píng)指正胚嘲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市洛二,隨后出現(xiàn)的幾起案子馋劈,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡陶耍,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)沉桌,“玉大人,你說(shuō)我怎么就攤上這事兼耀〗程猓” “怎么了郁季?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么侯嘀? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任诗茎,我火速辦了婚禮,結(jié)果婚禮上楚午,老公的妹妹穿的比我還像新娘。我一直安慰自己把沼,他們只是感情好篮奄,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著咖城,像睡著了一般辐董。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天贱迟,我揣著相機(jī)與錄音壤靶,去河邊找鬼忧换。 笑死刹缝,一個(gè)胖子當(dāng)著我的面吹牛梢夯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播勤篮,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼竟终,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年涩僻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了室抽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡堂鲜,死狀恐怖缔莲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鸵闪,我是刑警寧澤檐晕,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布伞矩,位于F島的核電站沟蔑,受9級(jí)特大地震影響厅须,放射性物質(zhì)發(fā)生泄漏憔儿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一奶陈、第九天 我趴在偏房一處隱蔽的房頂上張望易阳。 院中可真熱鬧,春花似錦吃粒、人聲如沸潦俺。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)事示。三九已至,卻和暖如春僻肖,著一層夾襖步出監(jiān)牢的瞬間肖爵,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工臀脏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留劝堪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓揉稚,卻偏偏與公主長(zhǎng)得像秒啦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子搀玖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348