Gestion des pics de trafic pour ventes flash en PHP

  1. Définition des ventes flash

Une vente flash est une opération promotionnele proposée par certaines plateformes e-commerce. En général, ces offres présentent les caractéristiques suivantes : quantité très limitée, prix extrêmement attractifs, et un délai d'achat exprimé en secondes. Par exemple, un produit valant plusieurs centaines ou milliers d'euros peut être proposé à un euro, mais un seul exemplaire disponible. L'achat débute à une heure précise, ce qui génère un afflux massif de visiteurs souhaitant obtenir ce produit. Plusieurs facteurs entrent en jeu pour réussi un tel achat : les performances informatiques, la qualité de la connexion réseau, et parfois simplement la chance.

  1. Défis techniques des ventes flash

2.1 Concurrence élevée

Les ventes flash les plus populaires attirent facilement plusieurs dizaines de milliers de utilisateurs simultanés. Cette affluence importante met à rude épreuve l'ensemble de l'infrastructure technique, depuis le serveur web frontal jusqu'à la base de données.

2.2 Sur-vente de produits

Tout produit dispose d'un stock limité. L'objectif principle consiste à garantir que le nombre de commandes validées ne dépasse jamais la quantité disponible. C'est un défi crucial pour toute opération de ce type.

  1. Solutions techniques

3.1 Coté frontend

A: Horizontalement

L'ajout de serveurs supplémentaires représente la solution la plus directe. L'augmentation de la capacité de traitement permet d'absorber les pics de trafic.

B: Staticisation

Tous les éléments statiques de la page promotionnelle doivent être générés de manière statique, en minimisant les éléments dynamiques. L'utilisation d'un CDN permet de distribuer la charge géographique.

C: Limitation de débit

Une limitatoin au niveau IP permet de contrôler le nombre de requêtes autorisées par unité de temps. Des mécanismes supplémentaires comme desCaptcha ou des questions quiz peuvent être utilisés pour espacer les requêtes.

3.2 Coté backend

A: Mécanismes de verrouillage

Le verrouillage optimiste repose sur un champ version dans la table de base de données. Chaque modification incrémente ce compteur, permettant de détecter les conflits lors de la soumission. Cette approche présente une limite : elle fonctionne uniquement au sein d'une même application.

Le verrouillage pessimiste locks directement les données au niveau de la base de données. Avec MySQL par exemple, l'utilisation de SELECT ... FOR UPDATE bloque l'accès aux autres transactions.

Les locks fichier permettent également de gérer les accès concurrents en verrouillant un fichier spécifique.

B: File d'attente Redis

L'intégration d'une file d'attente permet de traiter toutes les opérations d'écriture de manière séquentielle. Lorsque le seuil de stock est atteint, la consommation de la file s'arrête et l'achat est désactivé.

Avantages : résolution du problème de sur-vente, légère amélioration des performances.

Inconvénients : les performances dépendent du traitement le plus lent entre la file et la base de données. Pour plusieurs produits simultanément,多条文件 d'attente sont nécessaires.


  1. Exemples pratiques

4.1 Approche basique, risque de sur-vente

<?php
header("content-type:text/html;charset=utf-8");

$dsn='mysql:host=localhost;dbname=boutique';
try {
    $pdo = new PDO($dsn, 'utilisateur', 'motdepasse');
    $pdo->exec("set names utf8");
} catch (PDOException $erreur) {
    exit('Connexion échouée : ' . $erreur->getMessage());
}

$prix       = 10;
$id_client  = 1;
$id_produit = 1;
$ref_sku    = 11;
$quantite   = 1;

function generer_reference() {
    return date('ymd') . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}

$requete = "SELECT quantite FROM stock_produits WHERE produit_id = ? AND sku = ?";
$prep = $pdo->prepare($requete);
$prep->execute([$id_produit, $ref_sku]);
$donnees = $prep->fetch(PDO::FETCH_ASSOC);

if ($donnees['quantite'] > 0) {
    $reference = generer_reference();
    $sql = "INSERT INTO commandes(reference, client_id, produit_id, sku, prix) 
            VALUES(?, ?, ?, ?, ?)";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([$reference, $id_client, $id_produit, $ref_sku, $prix]);

    $sql2 = "UPDATE stock_produits SET quantite = quantite - ? WHERE sku = ?";
    $result = $pdo->prepare($sql2)->execute([$quantite, $ref_sku]);
    
    if ($result) {
        // Réduction effectuée
    } else {
        // Échec de la réduction
    }
} else {
    // Stock épuisé
}
echo "traitement_termine";

4.2 Utilisation du verrouillage fichier

<?php
header("content-type:text/html;charset=utf-8");

$dsn='mysql:host=localhost;dbname=boutique';
try {
    $pdo = new PDO($dsn, 'utilisateur', 'motdepasse');
    $pdo->exec("set names utf8");
} catch (PDOException $erreur) {
    exit('Connexion échouée : ' . $erreur->getMessage());
}

$prix       = 10;
$id_client  = 1;
$id_produit = 1;
$ref_sku    = 11;
$quantite   = 1;

function generer_reference() {
    return date('ymd') . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}

$requete = "SELECT quantite FROM stock_produits WHERE produit_id = ? AND sku = ?";
$prep = $pdo->prepare($requete);
$prep->execute([$id_produit, $ref_sku]);
$donnees = $prep->fetch(PDO::FETCH_ASSOC);

$pointeur = fopen("verrou.txt", "w+");
if (!flock($pointeur, LOCK_EX | LOCK_NB)) {
    echo "Serveur occupé, veuillez réessayer";
    return;
}

if ($donnees['quantite'] > 0) {
    $reference = generer_reference();
    $sql = "INSERT INTO commandes(reference, client_id, produit_id, sku, prix) 
            VALUES(?, ?, ?, ?, ?)";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([$reference, $id_client, $id_produit, $ref_sku, $prix]);

    $sql2 = "UPDATE stock_produits SET quantite = quantite - ? WHERE sku = ?";
    $result = $pdo->prepare($sql2)->execute([$quantite, $ref_sku]);
    
    if ($result) {
        flock($pointeur, LOCK_UN);
    }
} else {
    // Stock épuisé
}
fclose($pointeur);
echo "traitement_termine";

4.3 Implémentation avec file d'attente Redis

Partie cliente

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$cle_file = 'venteflash';
$cle_stock = 'stock_produit';

$identifiant = mt_rand(1000, 9999);
$stock_initial = 10;
usleep(100000);

if ($stock_initial > $redis->get($cle_stock)) {
    $redis->incr($cle_stock);
    $redis->lpush($cle_file, $identifiant);
}

$redis->close();

Partie serveur

<?php
header("content-type:text/html;charset=utf-8");

$dsn='mysql:host=localhost;dbname=boutique';
try {
    $pdo = new PDO($dsn, 'utilisateur', 'motdepasse');
    $pdo->exec("set names utf8");
} catch (PDOException $erreur) {
    exit('Connexion échouée : ' . $erreur->getMessage());
}

$prix       = 10;
$id_client  = 1;
$id_produit = 1;
$ref_sku    = 11;
$quantite   = 1;

function generer_reference() {
    return date('ymd') . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

while (true) {
    $client_id = $redis->rpop('venteflash');

    if (!$client_id) {
        continue;
    }

    $reference = generer_reference();
    $sql = "INSERT INTO commandes(reference, client_id, produit_id, sku, prix) 
            VALUES(?, ?, ?, ?, ?)";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([$reference, $client_id, $id_produit, $ref_sku, $prix]);

    $sql2 = "UPDATE stock_produits SET quantite = quantite - ? WHERE sku = ?";
    $result = $pdo->prepare($sql2)->execute([$quantite, $ref_sku]);
    
    if ($result) {
        // Réduction réussie
    }
}
$redis->close();


Outil de test de charge recommandé : JMeter

Étiquettes: PHP Redis MySQL Concurrence base de données

Publié le 6 juillet à 20h57