一年多以前在學(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)指正胚嘲。