Maison >développement back-end >tutoriel php >Un exemple d'utilisation de PHP combiné avec Redis pour réaliser des fonctions de snap-up et de vente flash avec une concurrence élevée
Les ventes instantanées et flash sont des scénarios d'application très courants de nos jours. Il y a deux problèmes principaux qui doivent être résolus :
1 La pression provoquée par une forte concurrence sur la base de données
2. Comment résoudre le problème dans des conditions de concurrence Réduction correcte des stocks (problème de "survente")
Pour le premier problème, il est déjà facile de penser à utiliser le cache pour gérer les ventes urgentes et éviter d'exploiter directement la base de données, comme comme en utilisant Redis.
L'accent est mis sur la deuxième question
Écriture conventionnelle :
Interrogez l'inventaire du produit correspondant pour voir s'il est supérieur à 0, puis effectuez des opérations telles que générer une commande, mais lors du jugement de l'inventaire, est-il supérieur à 0 ? S'il y a des problèmes en cas de concurrence élevée, entraînant un inventaire négatif
<?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('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8); } //记录日志 function insertLog($event,$type=0){ global $conn; $sql="insert into ih_log(event,type) values('$event','$type')"; mysql_query($sql,$conn); } //模拟下单操作 //库存是否大于0 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";//解锁 此时ih_store数据中goods_id='$goods_id' and sku_id='$sku_id' 的数据被锁住(注3),其它事务必须等待此次事务 提交后才能执行 $rs=mysql_query($sql,$conn); $row=mysql_fetch_assoc($rs); if($row['number']>0){//高并发下会导致超卖 $order_sn=build_order_no(); //生成订单 $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price) values('$order_sn','$user_id','$goods_id','$sku_id','$price')"; $order_rs=mysql_query($sql,$conn); //库存减少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs=mysql_query($sql,$conn); if(mysql_affected_rows()){ insertLog('库存减少成功'); }else{ insertLog('库存减少失败'); } }else{ insertLog('库存不够'); } ?>
Optimisation plan 1 : Modifier le champ du numéro de champ d'inventaire défini sur non signé, lorsque l'inventaire est à 0, car le champ ne peut pas être un nombre négatif, false sera renvoyé
//库存减少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0"; $store_rs=mysql_query($sql,$conn); if(mysql_affected_rows()){ insertLog('库存减少成功'); }
Plan d'optimisation 2 : Utiliser les transactions MySQL, les lignes pour les opérations de verrouillage
<?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('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8); } //记录日志 function insertLog($event,$type=0){ global $conn; $sql="insert into ih_log(event,type) values('$event','$type')"; mysql_query($sql,$conn); } //模拟下单操作 //库存是否大于0 mysql_query("BEGIN"); //开始事务 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行 $rs=mysql_query($sql,$conn); $row=mysql_fetch_assoc($rs); if($row['number']>0){ //生成订单 $order_sn=build_order_no(); $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price) values('$order_sn','$user_id','$goods_id','$sku_id','$price')"; $order_rs=mysql_query($sql,$conn); //库存减少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs=mysql_query($sql,$conn); if(mysql_affected_rows()){ insertLog('库存减少成功'); mysql_query("COMMIT");//事务提交即解锁 }else{ insertLog('库存减少失败'); } }else{ insertLog('库存不够'); mysql_query("ROLLBACK"); } ?>
Solution d'optimisation 3 : Utiliser des verrous exclusifs de fichiers non bloquants
<?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('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8); } //记录日志 function insertLog($event,$type=0){ global $conn; $sql="insert into ih_log(event,type) values('$event','$type')"; 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='$goods_id' and sku_id='$sku_id'"; $rs=mysql_query($sql,$conn); $row=mysql_fetch_assoc($rs); if($row['number']>0){//库存是否大于0 //模拟下单操作 $order_sn=build_order_no(); $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price) values('$order_sn','$user_id','$goods_id','$sku_id','$price')"; $order_rs=mysql_query($sql,$conn); //库存减少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs=mysql_query($sql,$conn); if(mysql_affected_rows()){ insertLog('库存减少成功'); flock($fp,LOCK_UN);//释放锁 }else{ insertLog('库存减少失败'); } }else{ insertLog('库存不够'); } fclose($fp);
Plan d'optimisation 4 : Utilisez la file d'attente redis, car l'opération pop est atomique, même si de nombreux utilisateurs arrivent en même temps, ils seront exécutés séquentiellement. est recommandé d'utiliser (les performances des transactions MySQL diminuent considérablement en cas de concurrence élevée, verrouillage de fichier La même méthode est utilisée)
Mettez d'abord l'inventaire du produit dans une file d'attente
<?php $store=1000; $redis=new Redis(); $result=$redis->connect('127.0.0.1',6379); $res=$redis->llen('goods_store'); echo $res; $count=$store-$res; for($i=0;$i<$count;$i++){ $redis->lpush('goods_store',1); } echo $redis->llen('goods_store'); ?>
Acheter et décrire la logique
<?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('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8); } //记录日志 function insertLog($event,$type=0){ global $conn; $sql="insert into ih_log(event,type) values('$event','$type')"; mysql_query($sql,$conn); } //模拟下单操作 //下单前判断redis队列库存量 $redis=new Redis(); $result=$redis->connect('127.0.0.1',6379); $count=$redis->lpop('goods_store'); if(!$count){ insertLog('error:no store redis'); return; } //生成订单 $order_sn=build_order_no(); $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price) values('$order_sn','$user_id','$goods_id','$sku_id','$price')"; $order_rs=mysql_query($sql,$conn); //库存减少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs=mysql_query($sql,$conn); if(mysql_affected_rows()){ insertLog('库存减少成功'); }else{ insertLog('库存减少失败'); }
Simuler 5000 tests à haute concurrence
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
Ce qui précède n'est qu'un simulation simple d'achat de panique sous forte concurrence. Le scénario réel est beaucoup plus compliqué que cela, et il y a beaucoup de choses auxquelles il faut prêter attention
Si la page d'achat précipité est rendue statique, l'interface est appelée via ajax
Ou comme ce qui précède amènera un utilisateur à effectuer plusieurs achats, l'idée est la suivante :
Nécessite une file d'attente, une file d'attente des résultats d'achat urgents et une file d'attente d'inventaire. En cas de concurrence élevée, entrez d'abord l'utilisateur dans la file d'attente, utilisez une boucle de thread pour supprimer un utilisateur de la file d'attente et déterminez si l'utilisateur est déjà dans la file d'attente des résultats d'achat urgent. Si c'est le cas, il a été cassé. vers le haut, sinon il n'est pas récupéré, l'inventaire est réduit de 1 et la base de données est écrite. Mettez l'utilisateur dans la file d'attente des résultats.
Tableau de données de test
-- -- 数据库: `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, '小米手机'); -- -------------------------------------------------------- -- -- 表的结构 `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 '0', `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 '0', `goods_id` int(11) NOT NULL DEFAULT '0', `sku_id` int(11) NOT NULL DEFAULT '0', `price` float NOT NULL, `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表' 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 '0', `number` int(10) NOT NULL DEFAULT '0', `freez` int(11) NOT NULL DEFAULT '0' COMMENT '虚拟库存', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存' AUTO_INCREMENT=2 ; -- -- 转存表中的数据 `ih_store` -- INSERT INTO `ih_store` (`id`, `goods_id`, `sku_id`, `number`, `freez`) VALUES (1, 1, 11, 500, 0);
Ce qui précède est le php de l'éditeur combiné avec Redis pour réaliser des achats urgents et des ventes flash sous une forte concurrence. Tous des exemples de fonctions sont inclus. J'espère que tout le monde prendra en charge le site Web PHP chinois ~
Pour plus d'exemples de PHP combinés avec Redis pour obtenir des fonctions de snap-up et de vente flash à haute concurrence, veuillez faire attention au PHP chinois site web!