>백엔드 개발 >PHP 튜토리얼 >높은 동시성에서 스냅업 및 플래시 세일 기능을 구현하기 위해 PHP와 Redis를 결합한 예

높은 동시성에서 스냅업 및 플래시 세일 기능을 구현하기 위해 PHP와 Redis를 결합한 예

高洛峰
高洛峰원래의
2016-12-26 14:16:332390검색

스냅업 및 플래시 세일은 오늘날 매우 일반적인 애플리케이션 시나리오입니다. 해결해야 할 두 가지 주요 문제가 있습니다.

1 데이터베이스의 높은 동시성으로 인한 압박

2 경쟁 상황에서 문제를 해결하는 방법 올바른 재고 감소("과매도" 문제)

첫 번째 문제의 경우 이미 캐시를 사용하여 급하게 구매를 처리하고 데이터베이스를 직접 운영하지 않는 것을 생각하기 쉽습니다. Redis를 사용하는 것처럼.

두 번째 질문에 초점이 맞춰져 있습니다

관례적 글쓰기:

해당 상품의 재고를 조회하여 0보다 큰지 확인한 후 다음과 같은 연산을 수행합니다. 주문을 생성했는데 재고를 판단할 때 0보다 큰가요? 높은 동시성에서 문제가 발생하여 마이너스 재고가 발생할 경우

<?php
$conn=mysql_connect("localhost","big","123456");
if(!$conn){
    echo "connect failed";
    exit;
}
mysql_select_db("big",$conn);
mysql_query("set names utf8");
 
$price=10;
$user_id=1;
$goods_id=1;
$sku_id=11;
$number=1;
 
//生成唯一订单
function build_order_no(){
  return date(&#39;ymd&#39;).substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
    global $conn;
    $sql="insert into ih_log(event,type)
    values(&#39;$event&#39;,&#39;$type&#39;)";
    mysql_query($sql,$conn);
}
 
//模拟下单操作
//库存是否大于0
$sql="select number from ih_store where goods_id=&#39;$goods_id&#39; and sku_id=&#39;$sku_id&#39;";//解锁 此时ih_store数据中goods_id=&#39;$goods_id&#39; and sku_id=&#39;$sku_id&#39; 的数据被锁住(注3),其它事务必须等待此次事务 提交后才能执行
$rs=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
if($row[&#39;number&#39;]>0){//高并发下会导致超卖
    $order_sn=build_order_no();
    //生成订单
    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
    values(&#39;$order_sn&#39;,&#39;$user_id&#39;,&#39;$goods_id&#39;,&#39;$sku_id&#39;,&#39;$price&#39;)";
    $order_rs=mysql_query($sql,$conn);
     
    //库存减少
    $sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39;";
    $store_rs=mysql_query($sql,$conn);
    if(mysql_affected_rows()){
        insertLog(&#39;库存减少成功&#39;);
    }else{
        insertLog(&#39;库存减少失败&#39;);
    }
}else{
    insertLog(&#39;库存不够&#39;);
}
?>

최적화 계획 1: 인벤토리 필드 번호 필드를 서명되지 않음으로 설정합니다. 인벤토리가 0인 경우 필드는 음수가 될 수 없으므로 false가 반환됩니다.

//库存减少
$sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39; and number>0";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
    insertLog(&#39;库存减少成功&#39;);
}

최적화 계획 2: MySQL 트랜잭션, 잠금 작업에 라인 사용

<?php
$conn=mysql_connect("localhost","big","123456");
if(!$conn){
    echo "connect failed";
    exit;
}
mysql_select_db("big",$conn);
mysql_query("set names utf8");
 
$price=10;
$user_id=1;
$goods_id=1;
$sku_id=11;
$number=1;
 
//生成唯一订单号
function build_order_no(){
  return date(&#39;ymd&#39;).substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
    global $conn;
    $sql="insert into ih_log(event,type)
    values(&#39;$event&#39;,&#39;$type&#39;)";
    mysql_query($sql,$conn);
}
 
//模拟下单操作
//库存是否大于0
mysql_query("BEGIN");   //开始事务
$sql="select number from ih_store where goods_id=&#39;$goods_id&#39; and sku_id=&#39;$sku_id&#39; FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行
$rs=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
if($row[&#39;number&#39;]>0){
    //生成订单
    $order_sn=build_order_no();
    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
    values(&#39;$order_sn&#39;,&#39;$user_id&#39;,&#39;$goods_id&#39;,&#39;$sku_id&#39;,&#39;$price&#39;)";
    $order_rs=mysql_query($sql,$conn);
     
    //库存减少
    $sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39;";
    $store_rs=mysql_query($sql,$conn);
    if(mysql_affected_rows()){
        insertLog(&#39;库存减少成功&#39;);
        mysql_query("COMMIT");//事务提交即解锁
    }else{
        insertLog(&#39;库存减少失败&#39;);
    }
}else{
    insertLog(&#39;库存不够&#39;);
    mysql_query("ROLLBACK");
}
?>

최적화 솔루션 3: 비차단 파일 독점 잠금 사용

<?php
$conn=mysql_connect("localhost","root","123456");
if(!$conn){
    echo "connect failed";
    exit;
}
mysql_select_db("big-bak",$conn);
mysql_query("set names utf8");
 
$price=10;
$user_id=1;
$goods_id=1;
$sku_id=11;
$number=1;
 
//生成唯一订单号
function build_order_no(){
  return date(&#39;ymd&#39;).substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
    global $conn;
    $sql="insert into ih_log(event,type)
    values(&#39;$event&#39;,&#39;$type&#39;)";
    mysql_query($sql,$conn);
}
 
$fp = fopen("lock.txt", "w+");
if(!flock($fp,LOCK_EX | LOCK_NB)){
    echo "系统繁忙,请稍后再试";
    return;
}
//下单
$sql="select number from ih_store where goods_id=&#39;$goods_id&#39; and sku_id=&#39;$sku_id&#39;";
$rs=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
if($row[&#39;number&#39;]>0){//库存是否大于0
    //模拟下单操作
    $order_sn=build_order_no();
    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
    values(&#39;$order_sn&#39;,&#39;$user_id&#39;,&#39;$goods_id&#39;,&#39;$sku_id&#39;,&#39;$price&#39;)";
    $order_rs=mysql_query($sql,$conn);
     
    //库存减少
    $sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39;";
    $store_rs=mysql_query($sql,$conn);
    if(mysql_affected_rows()){
        insertLog(&#39;库存减少成功&#39;);
        flock($fp,LOCK_UN);//释放锁
    }else{
        insertLog(&#39;库存减少失败&#39;);
    }
}else{
    insertLog(&#39;库存不够&#39;);
}
fclose($fp);

최적화 계획 4: redis queue를 사용합니다. pop 작업은 원자적이므로 많은 사용자가 동시에 도착하더라도 순차적으로 실행됩니다. 사용을 권장합니다 (높은 동시성에서는 mysql 트랜잭션 성능이 크게 떨어지며, 파일 잠금도 같은 방법을 사용합니다)

먼저 제품 인벤토리를 대기열에 넣습니다

<?php
$store=1000;
$redis=new Redis();
$result=$redis->connect(&#39;127.0.0.1&#39;,6379);
$res=$redis->llen(&#39;goods_store&#39;);
echo $res;
$count=$store-$res;
for($i=0;$i<$count;$i++){
    $redis->lpush(&#39;goods_store&#39;,1);
}
echo $redis->llen(&#39;goods_store&#39;);
?>

로직 구매 및 설명

<?php
$conn=mysql_connect("localhost","big","123456");
if(!$conn){
    echo "connect failed";
    exit;
}
mysql_select_db("big",$conn);
mysql_query("set names utf8");
 
$price=10;
$user_id=1;
$goods_id=1;
$sku_id=11;
$number=1;
 
//生成唯一订单号
function build_order_no(){
  return date(&#39;ymd&#39;).substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
    global $conn;
    $sql="insert into ih_log(event,type)
    values(&#39;$event&#39;,&#39;$type&#39;)";
    mysql_query($sql,$conn);
}
 
//模拟下单操作
//下单前判断redis队列库存量
$redis=new Redis();
$result=$redis->connect(&#39;127.0.0.1&#39;,6379);
$count=$redis->lpop(&#39;goods_store&#39;);
if(!$count){
    insertLog(&#39;error:no store redis&#39;);
    return;
}
 
//生成订单
$order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values(&#39;$order_sn&#39;,&#39;$user_id&#39;,&#39;$goods_id&#39;,&#39;$sku_id&#39;,&#39;$price&#39;)";
$order_rs=mysql_query($sql,$conn);
 
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39;";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
    insertLog(&#39;库存减少成功&#39;);
}else{
    insertLog(&#39;库存减少失败&#39;);
}

5000번의 높은 동시성 테스트 시뮬레이션

webbench -c 5000 -t 60 http://192.168.1.198/big/index.php
ab -r - n 6000 -c 5000 http://192.168.1.198/big/index.php

위 내용은 단지 높은 동시성에서의 패닉 구매에 대한 간단한 시뮬레이션입니다. 실제 시나리오는 이보다 훨씬 복잡하고 주의할 사항이 많습니다

스냅업 페이지가 정적으로 만들어지면 ajax를 통해 인터페이스를 호출합니다. 🎜>

또는 위의 내용을 사용하면 한 명의 사용자가 여러 개를 가져갈 수 있으므로 아이디어는 다음과 같습니다.

대기 대기열, 스냅업 결과 대기열 및 인벤토리 대기열이 필요합니다. 동시성이 높은 경우 먼저 사용자를 대기열에 입력하고 스레드 루프를 사용하여 대기열에서 사용자를 제거한 다음 해당 사용자가 이미 급매수 결과 대기열에 있는지 확인합니다. 그렇지 않으면 스냅업되지 않고 인벤토리가 1만큼 줄어들며 데이터베이스가 사용자를 결과 대기열에 기록합니다.

테스트 데이터 테이블

--
-- 数据库: `big`
--
 
-- --------------------------------------------------------
 
--
-- 表的结构 `ih_goods`
--
 
 
CREATE TABLE IF NOT EXISTS `ih_goods` (
  `goods_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `cat_id` int(11) NOT NULL,
  `goods_name` varchar(255) NOT NULL,
  PRIMARY KEY (`goods_id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;
 
 
--
-- 转存表中的数据 `ih_goods`
--
 
 
INSERT INTO `ih_goods` (`goods_id`, `cat_id`, `goods_name`) VALUES
(1, 0, &#39;小米手机&#39;);
 
-- --------------------------------------------------------
 
--
-- 表的结构 `ih_log`
--
 
CREATE TABLE IF NOT EXISTS `ih_log` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `event` varchar(255) NOT NULL,
 `type` tinyint(4) NOT NULL DEFAULT &#39;0&#39;,
 `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
 
--
-- 转存表中的数据 `ih_log`
--
 
 
-- --------------------------------------------------------
 
--
-- 表的结构 `ih_order`
--
 
CREATE TABLE IF NOT EXISTS `ih_order` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `order_sn` char(32) NOT NULL,
 `user_id` int(11) NOT NULL,
 `status` int(11) NOT NULL DEFAULT &#39;0&#39;,
 `goods_id` int(11) NOT NULL DEFAULT &#39;0&#39;,
 `sku_id` int(11) NOT NULL DEFAULT &#39;0&#39;,
 `price` float NOT NULL,
 `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=&#39;订单表&#39; AUTO_INCREMENT=1 ;
 
--
-- 转存表中的数据 `ih_order`
--
 
 
-- --------------------------------------------------------
 
--
-- 表的结构 `ih_store`
--
 
CREATE TABLE IF NOT EXISTS `ih_store` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `goods_id` int(11) NOT NULL,
 `sku_id` int(10) unsigned NOT NULL DEFAULT &#39;0&#39;,
 `number` int(10) NOT NULL DEFAULT &#39;0&#39;,
 `freez` int(11) NOT NULL DEFAULT &#39;0&#39; COMMENT &#39;虚拟库存&#39;,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=&#39;库存&#39; AUTO_INCREMENT=2 ;
 
--
-- 转存表中的数据 `ih_store`
--
 
INSERT INTO `ih_store` (`id`, `goods_id`, `sku_id`, `number`, `freez`) VALUES
(1, 1, 11, 500, 0);

위는 편집자의 PHP와 redis를 결합하여 높은 동시성으로 급매매와 플래시세일을 구현한 내용입니다. 함수 예제가 포함되어 있으니 모두들 PHP 중국어 홈페이지를 응원해주세요~

고동시성 스냅업 및 플래시 세일 기능을 달성하기 위해 Redis와 결합된 PHP의 더 많은 예제를 보려면 PHP 중국어에 주목하세요. 웹사이트!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.