新聞中心
本篇文章給大家?guī)砹岁P(guān)于php的相關(guān)知識(shí),其中主要介紹了關(guān)于在高并發(fā)情況下防止商品庫存超賣的相關(guān)問題,主要解決高并發(fā)對(duì)數(shù)據(jù)庫產(chǎn)生的壓力以及競爭狀態(tài)下如何解決商品庫存超賣,希望對(duì)大家有幫助。

商城系統(tǒng)中,搶購和秒殺是很常見的營銷場景,在一定時(shí)間內(nèi)有大量的用戶訪問商場下單,主要需要解決的問題有兩個(gè):
-
高并發(fā)對(duì)數(shù)據(jù)庫產(chǎn)生的壓力;
-
競爭狀態(tài)下如何解決商品庫存超賣;
高并發(fā)對(duì)數(shù)據(jù)庫產(chǎn)生的壓力
對(duì)于第一個(gè)問題,使用緩存來處理,避免直接操作數(shù)據(jù)庫,例如使用 Redis。
競爭狀態(tài)下如何解決商品庫存超賣
對(duì)于第二個(gè)問題,需要重點(diǎn)說明。
常規(guī)寫法:查詢出對(duì)應(yīng)商品的庫存,判斷庫存數(shù)量否大于 0,然后執(zhí)行生成訂單等操作,但是在判斷庫存是否大于 0 處,如果在高并發(fā)下就會(huì)有問題,導(dǎo)致庫存量出現(xiàn)負(fù)數(shù)。
測試表 sql
把如下表數(shù)據(jù)導(dǎo)入到數(shù)據(jù)庫中
/*
Navicat MySQL Data Transfer
Source Server : 01 本地localhost
Source Server Version : 50553
Source Host : localhost:3306
Source Database : test
Target Server Type : MYSQL
Target Server Version : 50553
File Encoding : 65001
Date: 2020-11-06 14:31:35
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for products
-- ----------------------------
DROP TABLE IF EXISTS `products`;
CREATE TABLE `products` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`title` varchar(50) DEFAULT NULL COMMENT '貨品名稱',
`store` int(11) DEFAULT '0' COMMENT '貨品庫存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='貨品表';
-- ----------------------------
-- Records of products
-- ----------------------------
INSERT INTO `products` VALUES ('1', '稻花香大米', '20');
-- ----------------------------
-- Table structure for order_log
-- ----------------------------
DROP TABLE IF EXISTS `order_log`;
CREATE TABLE `order_log` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`content` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '日志內(nèi)容',
`c_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- ----------------------------
-- Table structure for order
-- ----------------------------
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
`oid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '訂單號(hào)',
`product_id` int(11) DEFAULT '0' COMMENT '商品ID',
`number` int(11) DEFAULT '0' COMMENT '購買數(shù)量',
`c_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
PRIMARY KEY (`oid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COMMENT='訂單表';
下單處理代碼
0) {
sleep(1);
//step4 更新商品庫存數(shù)量(減去下單數(shù)量)
$sql = "update products set store=store-{$buy_num} where id={$product_id}";
if (mysqli_query($con, $sql)) {
echo "更新成功";
//step5 生成訂單號(hào)創(chuàng)建訂單
$oid = build_order_no();
create_order($oid, $product_id, $buy_num);
insertLog('庫存減少成功,下單成功');
} else {
echo "更新失敗";
insertLog('庫存減少失敗');
}
} else {
echo "沒有庫存";
insertLog('庫存不夠');
}
function db()
{
global $con;
$con = new mysqli('localhost','root','root','test');
if (!$con) {
echo "數(shù)據(jù)庫連接失敗";
}
}
/**
* 生成唯一訂單號(hào)
*/
function build_order_no()
{
return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
function create_order($oid, $product_id, $number)
{
global $con;
$sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
mysqli_query($con, $sql);
}
/**
* 記錄日志
*/
function insertLog($content)
{
global $con;
$sql = "INSERT INTO `order_log` (content) values('$content')";
mysqli_query($con, $sql);
}
將庫存字段字段設(shè)為 unsigned
因?yàn)閹齑孀侄尾荒転樨?fù)數(shù),在下單后更新商品庫存時(shí),如果出現(xiàn)負(fù)數(shù)將返回 false
0) {
sleep(1);
//step4 更新商品庫存數(shù)量(減去下單數(shù)量)
$sql = "update products set store=store-{$buy_num} where id={$product_id}";
if (mysqli_query($con, $sql)) {
echo "更新成功";
//step5 生成訂單號(hào)創(chuàng)建訂單
$oid = build_order_no();
create_order($oid, $product_id, $buy_num);
insertLog('庫存減少成功,下單成功');
} else {
// 如果出現(xiàn)負(fù)數(shù)將返回false
echo "更新失敗";
insertLog('庫存減少失敗');
}
} else {
//商品已經(jīng)搶購?fù)? echo "沒有庫存";
insertLog('庫存不夠');
}
function db()
{
global $con;
$con = new mysqli('localhost','root','root','test');
if (!$con) {
echo "數(shù)據(jù)庫連接失敗";
}
}
/**
* 生成唯一訂單號(hào)
*/
function build_order_no()
{
return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
function create_order($oid, $product_id, $number)
{
global $con;
$sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
mysqli_query($con, $sql);
}
/**
* 記錄日志
*/
function insertLog($content)
{
global $con;
$sql = "INSERT INTO `order_log` (content) values('$content')";
mysqli_query($con, $sql);
}
使用 mysql 的事務(wù),鎖住操作的行
在下單處理過程中,使用 mysql 的事務(wù)將正在下單商品行數(shù)據(jù)鎖定
0) {
sleep(1);
//step4 更新商品庫存數(shù)量(減去下單數(shù)量)
$sql = "update products set store=store-{$buy_num} where id={$product_id}";
if (mysqli_query($con, $sql)) {
echo "更新成功";
//step5 生成訂單號(hào)創(chuàng)建訂單
$oid = build_order_no();
create_order($oid, $product_id, $buy_num);
insertLog('庫存減少成功,下單成功');
mysqli_query($con, "COMMIT");//事務(wù)提交即解鎖
} else {
echo "更新失敗";
insertLog('庫存減少失敗');
mysqli_query($con, "ROLLBACK");//事務(wù)回滾即解鎖
}
} else {
//商品已經(jīng)搶購?fù)? echo "沒有庫存";
insertLog('庫存不夠');
mysqli_query($con, "ROLLBACK");//事務(wù)回滾即解鎖
}
function db()
{
global $con;
$con = new mysqli('localhost','root','root','test');
if (!$con) {
echo "數(shù)據(jù)庫連接失敗";
}
}
/**
* 生成唯一訂單號(hào)
*/
function build_order_no()
{
return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
function create_order($oid, $product_id, $number)
{
global $con;
$sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
mysqli_query($con, $sql);
}
/**
* 記錄日志
*/
function insertLog($content)
{
global $con;
$sql = "INSERT INTO `order_log` (content) values('$content')";
mysqli_query($con, $sql);
}
使用非阻塞的文件排他鎖
在處理下單請(qǐng)求的時(shí)候,用 flock 鎖定一個(gè)文件,如果鎖定失敗說明有其他訂單正在處理,此時(shí)要么等待要么直接提示用戶” 服務(wù)器繁忙”,計(jì)數(shù)器存儲(chǔ)搶購的商品數(shù)量,避免查詢數(shù)據(jù)庫。
阻塞 (等待) 模式:并發(fā)時(shí),當(dāng)有第二個(gè)用戶請(qǐng)求時(shí),會(huì)等待第一個(gè)用戶請(qǐng)求完成、釋放鎖,獲得文件鎖之后,程序才會(huì)繼續(xù)運(yùn)行下去。
0) {
//處理訂單
sleep(1);
//step4 更新商品庫存數(shù)量(減去下單數(shù)量)
$sql = "update products set store=store-{$buy_num} where id={$product_id}";
if (mysqli_query($con, $sql)) {
echo "更新成功";
//step5 生成訂單號(hào)創(chuàng)建訂單
$oid = build_order_no();
create_order($oid, $product_id, $buy_num);
insertLog('庫存減少成功,下單成功');
} else {
echo "更新失敗";
insertLog('庫存減少失敗');
}
} else {
//商品已經(jīng)搶購?fù)? echo "沒有庫存";
insertLog('庫存不夠');
}
flock($fp, LOCK_UN); //釋放鎖
}
fclose($fp);
function db()
{
global $con;
$con = new mysqli('localhost','root','root','test');
if (!$con) {
echo "數(shù)據(jù)庫連接失敗";
}
}
/**
* 生成唯一訂單號(hào)
*/
function build_order_no()
{
return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
function create_order($oid, $product_id, $number)
{
global $con;
$sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
mysqli_query($con, $sql);
}
/**
* 記錄日志
*/
function insertLog($content)
{
global $con;
$sql = "INSERT INTO `order_log` (content) values('$content')";
mysqli_query($con, $sql);
}
非阻塞模式:并發(fā)時(shí),第一個(gè)用戶請(qǐng)求,拿得文件鎖之后。后面請(qǐng)求的用戶直接返回系統(tǒng)繁忙,請(qǐng)稍后再試
0) {
//處理訂單
sleep(1);
//step4 更新商品庫存數(shù)量(減去下單數(shù)量)
$sql = "update products set store=store-{$buy_num} where id={$product_id}";
if (mysqli_query($con, $sql)) {
echo "更新成功";
//step5 生成訂單號(hào)創(chuàng)建訂單
$oid = build_order_no();
create_order($oid, $product_id, $buy_num);
insertLog('庫存減少成功,下單成功');
} else {
echo "更新失敗";
insertLog('庫存減少失敗');
}
} else {
//商品已經(jīng)搶購?fù)? echo "沒有庫存";
insertLog('庫存不夠');
}
flock($fp, LOCK_UN); //釋放鎖
} else {
//系統(tǒng)繁忙,請(qǐng)稍后再試
echo "系統(tǒng)繁忙,請(qǐng)稍后再試";
insertLog('系統(tǒng)繁忙,請(qǐng)稍后再試');
}
fclose($fp);
function db()
{
global $con;
$con = new mysqli('localhost','root','root','test');
if (!$con) {
echo "數(shù)據(jù)庫連接失敗";
}
}
/**
* 生成唯一訂單號(hào)
*/
function build_order_no()
{
return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
function create_order($oid, $product_id, $number)
{
global $con;
$sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
mysqli_query($con, $sql);
}
/**
* 記錄日志
*/
function insertLog($content)
{
global $con;
$sql = "INSERT INTO `order_log` (content) values('$content')";
mysqli_query($con, $sql);
}
使用 redis 隊(duì)列
-
因?yàn)?pop 操作是原子的,即使有很多用戶同時(shí)到達(dá),也是依次執(zhí)行,推薦使用
-
mysql 事務(wù)在高并發(fā)下性能下降很厲害,文件鎖的方式也是
1.先將商品庫存到 redis 隊(duì)列
connect('127.0.0.1', 6379);
$key = 'goods_store_' . $product_id;
$res = $redis->llen($key);
$count = $store - $res;
for ($i=0; $i<$count; $i++) {
$redis->lpush($key, 1);
}
echo $redis->llen($key);
function db()
{
global $con;
$con = new mysqli('localhost','root','root','test');
if (!$con) {
echo "數(shù)據(jù)庫連接失敗";
}
}
2. 搶購、秒殺邏輯
connect('127.0.0.1',6379);
$count = $redis->lpop('goods_store_' . $product_id);
if (!$count) {
insertLog('error:no store redis');
return '秒殺結(jié)束,沒有商品庫存了';
}
sleep(1);
//step3 更新商品庫存數(shù)量(減去下單數(shù)量)
$sql = "update products set store=store-{$buy_num} where id={$product_id}";
if (mysqli_query($con, $sql)) {
echo "更新成功";
//step4 生成訂單號(hào)創(chuàng)建訂單
$oid = build_order_no();
create_order($oid, $product_id, $buy_num);
insertLog('庫存減少成功,下單成功');
} else {
echo "更新失敗";
insertLog('庫存減少失敗');
}
function db()
{
global $con;
$con = new mysqli('localhost','root','root','test');
if (!$con) {
echo "數(shù)據(jù)庫連接失敗";
}
}
/**
* 生成唯一訂單號(hào)
*/
function build_order_no()
{
return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
function create_order($oid, $product_id, $number)
{
global $con;
$sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
mysqli_query($con, $sql);
}
/**
* 記錄日志
*/
function insertLog($content)
{
global $con;
$sql = "INSERT INTO `order_log` (content) values('$content')";
mysqli_query($con, $sql);
}
redis 樂觀鎖防止超賣
connect("127.0.0.1", 6379);
$redis->watch('sales');//樂觀鎖 監(jiān)視作用 set() 初始值0
$sales = $redis->get('sales');
$n = 20;// 庫存
if ($sales >= $n) {
exit('秒殺結(jié)束');
}
//redis開啟事務(wù)
$redis->multi();
$redis->incr('sales'); //將 key 中儲(chǔ)存的數(shù)字值增一 ,如果 key 不存在,那么 key 的值會(huì)先被初始化為 0 ,然后再執(zhí)行 INCR 操作。
$res = $redis->exec(); //成功1 失敗0
if ($res) {
//秒殺成功
$con = new mysqli('localhost','root','root','test');
if (!$con) {
echo "數(shù)據(jù)庫連接失敗";
}
$product_id = 1;// 商品ID
$buy_num = 1;// 購買數(shù)量
sleep(1);
$sql = "update products set store=store-{$buy_num} where id={$product_id}";
if (mysqli_query($con, $sql)) {
echo "秒殺完成";
}
} else {
exit('搶購失敗');
} 新聞標(biāo)題:PHP高并發(fā)情形下怎么防止商品庫存超賣
地址分享:http://m.fisionsoft.com.cn/article/djhjghg.html


咨詢
建站咨詢
