Comment résoudre le problème de la haute simultanéité (ventes flash de produits) en PHP ? Deux solutions partagées

Libérer: 2023-04-10 22:04:01
avant
7109 Les gens l'ont consulté

Comment résoudre le problème de haute simultanéité (ventes flash de produits) en PHP ? L'article suivant partagera avec vous deux solutions (basées sur mysql ou basées sur Redis), j'espère qu'il vous sera utile.

Comment résoudre le problème de la haute simultanéité (ventes flash de produits) en PHP ? Deux solutions partagées

Le deuxième meurtre produira une simultanéité élevée instantanée. L'utilisation de la base de données augmentera la pression d'accès de la base de données et réduira la vitesse d'accès, nous devrions donc utiliser la mise en cache pour réduire la pression d'accès de la base de données

Vous pouvez voir ; le fonctionnement ici est différent de la commande originale : la précommande de vente flash générée ne sera pas écrite immédiatement dans la base de données, mais sera d'abord écrite dans le cache. Lorsque l'utilisateur paiera avec succès, le statut sera modifié et écrit dans. la base de données.

Supposons que num soit un champ stocké dans la base de données, qui stocke la quantité restante du produit flash-killé.

if($num > 0){
  //用户抢购成功,记录用户信息
  $num--;
}
Copier après la connexion

Supposons que dans un scénario à forte concurrence, lorsque la valeur de num dans la base de données est 1, plusieurs processus peuvent lire que num vaut 1 en même temps. Le programme détermine que les conditions sont remplies, le snap-up est. réussi, et le nombre est réduit de un.

Cela entraînera une livraison excessive de produits. À l'origine, seuls 10 produits pouvaient être récupérés, mais plus de 10 personnes pourraient les récupérer. À ce moment-là, le nombre sera négatif une fois la ruée terminée.

Il existe de nombreuses solutions à ce problème, qui peuvent être simplement divisées en solutions basées sur mysql et redis. Les performances de redis sont dues à mysql, il peut donc supporter une concurrence plus élevée. Cependant, les solutions présentées ci-dessous sont toutes basées sur un. MySQL unique et pour Redis, une concurrence plus élevée nécessite des solutions distribuées, ce qui n'est pas abordé dans cet article.

1. Solution basée sur mysql

Table des produits marchandises

CREATE TABLE `goods` (
 `id` int(11) NOT NULL,
 `num` int(11) DEFAULT NULL,
 `version` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Copier après la connexion

Journal de la table des résultats d'achat

CREATE TABLE `log` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `good_id` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Copier après la connexion

①Verrouillage pessimiste

La solution de verrouillage pessimiste utilise une lecture exclusive, c'est-à-dire qu'il ne peut y en avoir qu'un à la fois time Le processus lit la valeur de num. Une fois la transaction validée ou annulée, le verrou sera libéré et d'autres processus pourront le lire.

Cette solution est la plus simple et la plus facile à comprendre. Vous pouvez utiliser cette solution directement lorsque les exigences de performances ne sont pas élevées. Il est à noter que SELECT … FOR UPDATE doit utiliser les index autant que possible afin de verrouiller le moins de lignes possible SELECT … FOR UPDATE要尽可能的使用索引,以便锁定尽可能少的行数;

排他锁是在事务执行结束之后才释放的,不是读取完成之后就释放,因此使用的事务应该尽可能的早些提交或回滚,以便早些释放排它锁。

$this->mysqli->begin_transaction();
$result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1 FOR UPDATE");
$row = $result->fetch_assoc();
$num = intval($row['num']);
if($num > 0){
  usleep(100);
  $this->mysqli->query("UPDATE goods SET num=num-1");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  $this->mysqli->commit();
  echo "fail3:".$num;
}
Copier après la connexion

②乐观锁

乐观锁的方案在读取数据是并没有加排他锁,而是通过一个每次更新都会自增的version字段来解决,多个进程读取到相同num,然后都能更新成功的问题。在每个进程读取num的同时,也读取version的值,并且在更新num的同时也更新version,并在更新时加上对version的等值判断。

假设有10个进程都读取到了num的值为1,version值为9,则这10个进程执行的更新语句都是UPDATE goods SET num=num-1,version=version+1 WHERE version=9

Le verrou exclusif n'est libéré qu'une fois l'exécution de la transaction terminée, pas après ; la lecture est terminée. Elle est libérée plus tard, la transaction utilisée doit donc être validée ou annulée le plus tôt possible afin de libérer le verrou exclusif plus tôt.

$result = $this->mysqli->query("SELECT num,version FROM goods WHERE id=1 LIMIT 1");
$row = $result->fetch_assoc();
$num = intval($row['num']);
$version = intval($row['version']);
if($num > 0){
  usleep(100);
  $this->mysqli->begin_transaction();
  $this->mysqli->query("UPDATE goods SET num=num-1,version=version+1 WHERE version={$version}");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  echo "fail3:".$num;
}
Copier après la connexion

②Verrouillage optimiste

La solution de verrouillage optimiste n'ajoute pas de verrou exclusif lors de la lecture des données, mais le résout via un champ de version qui augmentera automatiquement à chaque mise à jour. Plusieurs processus liront le même numéro, et. alors le problème peut être mis à jour avec succès. Lorsque chaque processus lit num, il lit également la valeur de version lors de la mise à jour de num, il met également à jour la version et ajoute un jugement d'équivalence sur la version lors de la mise à jour.

Supposons que 10 processus ont lu que la valeur de num est 1 et la valeur de version est 9. Ensuite, les instructions de mise à jour exécutées par ces 10 processus sont toutes des UPDATE marchandises SET num=num-1,version=version+ 1 WHERE version=9,

Cependant, lorsqu'un des processus est exécuté avec succès, la valeur de version dans la base de données deviendra 10 et les 9 processus restants ne seront pas exécutés avec succès, garantissant ainsi que le produit sera ne sera pas sur-livré, la valeur de num ne sera pas inférieure à 0, mais cela entraîne également un problème, c'est-à-dire que les utilisateurs qui ont émis une demande de snap-up plus tôt ne pourront peut-être pas la récupérer, mais seront saisis. par des demandes ultérieures.

$result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1");
$row = $result->fetch_assoc();
$num = intval($row['num']);
if($num > 0){
  usleep(100);
  $this->mysqli->begin_transaction();
  $this->mysqli->query("UPDATE goods SET num=num-1 WHERE num>0");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  echo "fail3:".$num;
}
Copier après la connexion

③condition Where (opération atomique)

Le schéma de verrouillage pessimiste garantit que la valeur de num dans la base de données ne peut être lue et traitée que par un seul processus à la fois, c'est-à-dire que les processus de lecture simultanés doivent faites la queue ici.

Schéma de verrouillage optimiste Bien que la valeur de num puisse être lue par plusieurs processus en même temps, le jugement d'équivalence de la version dans l'opération de mise à jour peut garantir qu'une seule mise à jour des opérations de mise à jour simultanées peut réussir en même temps.

Il existe également une solution plus simple, qui consiste à ajouter uniquement la restriction conditionnelle de num>0 lors de l'opération de mise à jour. Bien que la solution restreinte par la condition Where semble similaire à la solution de verrouillage optimiste et puisse empêcher l'apparition de problèmes d'émission excessive, les performances sont toujours très différentes lorsque num est grand. Supposons que num soit 10 à ce moment-là et que 5 processus lisent num=10 en même temps. Pour le schéma de verrouillage optimiste, en raison du jugement d'égalité du champ de version, un seul de ces 5 processus sera mis à jour avec succès, et ces 5 processus s'exécuteront après l'achèvement, num est 9 ;

Pour la solution de jugement de condition Where, tant que num>0 peut être mis à jour avec succès, num sera 5 une fois l'exécution de ces 5 processus terminée.

$num = $this->redis->get('num');
if($num > 0) {
  $this->redis->watch('num');
  usleep(100);
  $res = $this->redis->multi()->decr('num')->lPush('result',$num)->exec();
  if($res == false){
    echo "fail1";
  }else{
    echo "success:".$num;
  }
}else{
  echo "fail2";
}
Copier après la connexion

2. Solution basée sur Redis

① Schéma de verrouillage optimiste basé sur la montre

🎜🎜watch est utilisé pour surveiller une (ou plusieurs) clé si cette (ou ces) clé est avant la transaction. exécuté Si modifié par d'autres commandes, la transaction sera interrompue. 🎜🎜Ce schéma est similaire au schéma de verrouillage optimiste de MySQL, et les performances spécifiques sont les mêmes. 🎜
public function init(){
  $this->redis->del('goods');
  for($i=1;$i<=10;$i++){
    $this->redis->lPush(&#39;goods&#39;,$i);
  }
  $this->redis->del(&#39;result&#39;);
  echo &#39;init done&#39;;
}
public function run(){
  $goods_id = $this->redis->rPop(&#39;goods&#39;);
  usleep(100);
  if($goods_id == false) {
    echo "fail1";
  }else{
    $res = $this->redis->lPush(&#39;result&#39;,$goods_id);
    if($res == false){
      echo "writelog:".$goods_id;
    }else{
      echo "success".$goods_id;
    }
  }
}
Copier après la connexion
Copier après la connexion
🎜🎜🎜②Solution de file d'attente basée sur une liste🎜🎜🎜

基于队列的方案利用了redis出队操作的原子性,抢购开始之前首先将商品编号放入响应的队列中,在抢购时依次从队列中弹出操作,这样可以保证每个商品只能被一个进程获取并操作,不存在超发的情况。

该方案的优点是理解和实现起来都比较简单,缺点是当商品数量较多是,需要将大量的数据存入到队列中,并且不同的商品需要存入到不同的消息队列中。

public function init(){
  $this->redis->del(&#39;goods&#39;);
  for($i=1;$i<=10;$i++){
    $this->redis->lPush(&#39;goods&#39;,$i);
  }
  $this->redis->del(&#39;result&#39;);
  echo &#39;init done&#39;;
}
public function run(){
  $goods_id = $this->redis->rPop(&#39;goods&#39;);
  usleep(100);
  if($goods_id == false) {
    echo "fail1";
  }else{
    $res = $this->redis->lPush(&#39;result&#39;,$goods_id);
    if($res == false){
      echo "writelog:".$goods_id;
    }else{
      echo "success".$goods_id;
    }
  }
}
Copier après la connexion
Copier après la connexion

③基于decr返回值的方案

如果我们将剩余量num设置为一个键值类型,每次先get之后判断,然后再decr是不能解决超发问题的。

但是redis中的decr操作会返回执行后的结果,可以解决超发问题。我们首先get到num的值进行第一步判断,避免每次都去更新num的值,然后再对num执行decr操作,并判断decr的返回值,如果返回值不小于0,这说明decr之前是大于0的,用户抢购成功。

public function run(){
  $num = $this->redis->get(&#39;num&#39;);
  if($num > 0) {
    usleep(100);
    $retNum = $this->redis->decr(&#39;num&#39;);
    if($retNum >= 0){
      $res = $this->redis->lPush(&#39;result&#39;,$retNum);
      if($res == false){
        echo "writeLog:".$retNum;
      }else{
        echo "success:".$retNum;
      }
    }else{
      echo "fail1";
    }
  }else{
    echo "fail2";
  }
}
Copier après la connexion

④基于setnx的排它锁方案

redis没有像mysql中的排它锁,但是可以通过一些方式实现排它锁的功能,就类似php使用文件锁实现排它锁一样。

setnx实现了exists和set两个指令的功能,若给定的key已存在,则setnx不做任何动作,返回0;若key不存在,则执行类似set的操作,返回1。

我们设置一个超时时间timeout,每隔一定时间尝试setnx操作,如果设置成功就是获得了相应的锁,执行num的decr操作,操作完成删除相应的key,模拟释放锁的操作。

public function run(){
  do {
    $res = $this->redis->setnx("numKey",1);
    $this->timeout -= 100;
    usleep(100);
  }while($res == 0 && $this->timeout>0);
  if($res == 0){
    echo &#39;fail1&#39;;
  }else{
    $num = $this->redis->get(&#39;num&#39;);
    if($num > 0) {
      $this->redis->decr(&#39;num&#39;);
      usleep(100);
      $res = $this->redis->lPush(&#39;result&#39;,$num);
      if($res == false){
        echo "fail2";
      }else{
        echo "success:".$num;
      }
    }else{
      echo "fail3";
    }
    $this->redis->del("numKey");
  }
}
Copier après la connexion

推荐学习:《PHP视频教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:微信公众号- PHP自学中心
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!