php和mysql实现抢购功能_《⾼并发秒杀抢购系统设计》PHP
⽰例代码
⼀年多以前在学校分享过⼀次《⾼并发秒杀抢购系统设计》,其中有部分⽰例代码未能贴出,因为当时⼯作换电脑导致程序代码丢失,⼀直就没有贴出来,到编写本⽂时有不少朋友向我要过代码,很不好意思⼀直没整理就没给,近期有时间就整理了⼀下。时间有点久了,⼀些内容细节有些忘记,⽰例代码处理模型如有考虑不到之处,请留⾔给我,我会跟进测试修改,提前谢谢各位。
没有看过上⼀篇⽂章的,可以先看看⼀次分享《⾼并发秒杀抢购系统设计》。
本次整理代码所⽤的相关程序版本:
PHP5.6加pthreads、redis、mysql扩展
Mysql5.7 ,不过⽤不到数据库的⾼级特性,5.0及以上版本⽀持Innodb存储引擎的就可以
Redis5.0.5
Centos7.8
尝试了PHP7.3和7.4的多线程,⽆论是pthreads还是parallel都出现“段错误”⽆法正常执⾏,可能和Centos环境有些关系,有能执⾏成功的朋友请指教⼀下。
准备⼯作,建库建表,test库就⾏,建表语句:
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);
新⼿错误代码
先看新⼿最容易犯错的代码,它的处理逻辑在单进程单线程没有并发的情况下是对的,但是在⾼并发下就是错误的。
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() {
//模拟真实环境,连接数据库,每次都返回⼀个新的数据库连接
$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);
//获取库存余量
$total = $info['total'];
echo 'tid='.self::getCurrentThreadId().' total='.$total."\n";
if($total > 0) {//判断库存是否还有
/
*
* 这⾥会出现两种写法,但是结果都⼀样,都是错误的
* ⼀种是直接数据库字段减1
* 另⼀种是取出的库存数减1再写回数据库
*/
// 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();
}
//获取结果
$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,⼀个线程模拟⼀个⽤户下单减库存,100个库存需要100个线程,按正常逻辑100个线程执⾏完毕库存是0就对。上⾯这段代码可直接复制到⼀个PHP⽂件,修改顶部的Mysql配置,然后多次执⾏(⼀定要多次快速执⾏),你能够发现好多时候最后库存⼤于0,有的线程读取到了相同的库存。
分析⼀下:100个库存,100个⽤户都已经完成下单,还有剩余,继续执⾏的话⼀定是要超卖了~~~
悲观锁,利⽤Mysql实现
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数据库实现悲观锁
class PessimisticLock extends Thread {
public function run() {
//模拟真实环境,连接数据库,每次都返回⼀个新的数据库连接
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
//从数据库中取出库存 利⽤Innodb更新⾏锁实现悲观锁
$sql = "update goods set total=total-1 where id=1 and total>0";
$result = mysql_query($sql,$mysql);
//要检查修改影响的条数,执⾏成功但不⼀定修改数据
$affectedRows = $result ? mysql_affected_rows() : 0;
if($affectedRows) {//根据修改影响的条数进⾏后续操作
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();
}
//获取结果
$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";
这段代码多次使劲执⾏,最后库存都是0,所以这个⽅法原理上可以,也只是原理上可以,不建议直接⽤在⾼并发系统上,主要因为它会⼤幅度增加数据库负载。我们对系统优化⼀般⾸先着⼿的都是减少数据库的直接操作,因此这个⽅法不建议,真要⽤还需要看具体情况。
乐观锁,利⽤Redis的事务来实现
error_reporting(E_ALL ^ E_DEPRECATED);
class Conf {
public static $host = 'localhost';
}
//利⽤redis事务实现乐观锁
class OptimisticLock extends Thread {
public function run() {
$redis = new Redis();
$redis->connect(Conf::$host);
do {//只要还有库存且没成功减库存就⼀直执⾏
$goodsTotal = $redis->get('goods_total');
echo self::getCurrentThreadId().' total='.$goodsTotal."\n";
if($goodsTotal <= 0) break;//每次都检查是否还有库存 没有库存退出循环
php支持多线程吗
$redis->watch('goods_total');
$redis->multi();
$redis->decr('goods_total');
$res = $redis->exec();
} while(!$res);
}
}
//初始化缓存库存数据
$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();
}
这段代码使劲多次执⾏,最后库存也是0,所以也是可⾏的,这个⽅法也是⾸先推荐的⽅法,内存中的数据操作⽐在数据库中要快得多,负载能⼒会⾼跟多。
本分享给出的⽰例代码只是处理逻辑,具体应⽤还要根据具体服务器架构甚⾄是业务逻辑进⾏调整。有不⾜之处欢迎批评指正。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。