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;//每次都检查是否还有库存 没有库存退出循环

$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,所以也是可行的,这个方法也是首先推荐的方法,内存中的数据操作比在数据库中要快得多,负载能力会高跟多。

本分享给出的示例代码只是处理逻辑,具体应用还要根据具体服务器架构甚至是业务逻辑进行调整。有不足之处欢迎批评指正。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值