2015-04-17 7 views
9

Eseguo un sito di coupon che visualizza 50-70 richieste/sec al momento del lancio delle nostre offerte (lanciamo più di 20 offerte contemporaneamente più volte al giorno). Quando gli affari vengono pubblicati, i nostri utenti premono un pulsante per richiedere un coupon per un prodotto specifico, che serve un codice coupon univoco tramite una richiesta https di ajax. Ogni coupon può essere riscattato solo una volta.Come distribuire codici coupon univoci con 70 richieste/sec utilizzando Node.js

Il mio problema è che con un volume di traffico così elevato in questi orari, lo stesso coupon può essere distribuito a più utenti. Questo è un problema dato che solo uno di questi sarà in grado di riscattare il coupon in partenza per una scarsa esperienza utente per l'altro.

Sto memorizzando tutte le informazioni sul coupon in oggetti in memoria su un server node.js ospitato da IBM Bluemix. Ho pensato che questo mi avrebbe permesso di gestire le richieste rapidamente.

Come devo conservare informazioni coupon:

global.coupons = {}; 

//the number of coupons given for each product 
global.given = {}; 

/* Setting the coupon information */ 

//....I query my database for the products to be given today 

for(var i = 0; i < results.length; i++){ 
    var product = results[i]; 

    //add only the coupons to give today to the array 
    var originalCoups = product.get('coupons'); 
    var numToTake = product.get('toGivePerDay'); 

     if(product.get('givenToday') > 0){ 
      numToTake = numToTake - product.get('givenToday'); 
     } 
     // Example coupon array [["VVXM-Q577J2-XRGHCC","VVLE-JJR364-5G5Q6B"]] 
     var couponArray = originalCoups[0].splice(product.get('given'), numToTake); 

     //set promo info 
     global.coupons[product.id] = couponArray; 
     global.given[product.id] = 0; 
} 

Maniglia Coupon Richiesta:

app.post('/getCoupon', urlencodedParser, function(req, res){ 
    if (!req.body) return res.status(400).send("Bad Request"); 
    if (!req.body.category) return res.status(200).send("Please Refresh the Page."); 

     //Go grab a coupon 
     var coupon = getUserACoupon(req.body.objectId); 

     res.type('text/plain'); 
     res.status(200).send(coupon); 

     if(coupon != "Sold Out!" && coupon != "Bad Request: Object does not exist."){ 

      //Update user & product analytics 
      setStatsAfterCouponsSent(req.body.objectId, req.body.sellerProduct, req.body.userEmail, req.body.purchaseProfileId, coupon, req.body.category); 

     } 
}); 

//getCoupon logic 
function getUserACoupon(objectId){ 

    var coupToReturn; 

    // coupon array for the requseted product 
    var coupsArray = global.coupons[objectId]; 

    if(typeof coupsArray != 'undefined'){ 

     // grab the number of coupons already given for this product and increase by one 
     var num = global.given[objectId]; 
     global.given[objectId] = num+1; 

     if(num < coupsArray.length){ 
      if(coupsArray[num] != '' && typeof coupsArray[num] != 'undefined' && coupsArray[num] != 'undefined'){ 

       coupToReturn = coupsArray[num]; 

      }else{ 
       console.log("Error with the coupon for "+objectId + " the num is " + num); 
       coupToReturn = "Sold Out!"; 
       wasSoldOut(objectId); 
      } 
     }else{ 
      console.log("Sold out "+objectId+" with num " + num); 
      coupToReturn = "Sold Out!"; 
      wasSoldOut(objectId); 
     } 
    }else{ 
     coupToReturn = "Bad Request: Object does not exist."; 
     wasSoldOut(objectId); 
    } 
    return coupToReturn; 
} 

non ho una tonnellata di comprensione dei server Node.js e il loro funzionamento.

Come sempre, grazie per l'aiuto!

+0

Node.js è solo a thread singolo, quindi non dovrebbe essere un problema generare coupon univoci assicurandosi che siano diversi da qualsiasi valore passato. Il problema sarebbe quello di rendere il confronto rapido in quanto i cedolini passati accumulati sarebbero cresciuti solo nel tempo. Solo spitballing qui, ma penso che l'aggiornamento di un hash con il nuovo valore ogni volta dovrebbe garantire l'unicità del nuovo hash? – laggingreflex

+0

@laggingreflex I coupon non vengono generati sul server. I coupon sono generati da Amazon e quindi memorizzati in un array nel mio database. Il problema consiste semplicemente nel fare in modo che io stia afferrando un coupon dall'oggetto global.coupons solo una volta. Il motivo per cui ho iniziato a utilizzare Node.js era perché è a thread singolo, quindi ho pensato che non avrei incontrato questo problema, ma mi sono dimostrato sbagliato. – cgauss

risposta

5

Il problema si trova nella natura non bloccante/asincrona del nodo. Le chiamate alla stessa funzione da richieste simultanee non si attendono l'una per l'altra. Un sacco di richieste arrivano e contemporaneamente accedere alla matrice di codici globali.

Distribuisci lo stesso codice più volte perché lo counter is incremented by multiple requests concurrently in modo che possa accadere che più richieste visualizzino lo stesso stato contatore.

Un approccio per gestire il problema di concorrenza è quello di consentire ad un solo accesso (per getUserACoupon nel caso) per volta, in modo che parte di esecuzione in cui si consuma un coupon è sincronizzato o mutuamente esclusive. Un modo per ottenere ciò è un meccanismo di blocco, quindi quando una richiesta ottiene l'accesso al blocco, ulteriori richieste attendono fino al rilascio del blocco. In pseudo codice potrebbe essere simile a questa:

wait until lock exists 
create lock 
if any left, consume one coupon 
remove lock 

Ma questo approccio va contro la natura non-blocking del Nodo e anche introduce il problema di chi ottiene il blocco quando viene rilasciato se più di una richiesta stava aspettando.

Un modo migliore è più probabile un sistema di coda. Dovrebbe funzionare così un codice non consumato al momento della richiesta ma inserito in una coda come chiamabile, in attesa di dare il via. Puoi leggere la lunghezza della coda e smettere di accettare nuove richieste ("esaurito"), tuttavia questo sarà ancora simultaneo su una coda/contatore globale, quindi potresti finire con qualche altro oggetto in coda rispetto ai tagliandi, ma questo è non è un problema perché la coda sarà elaborata in modo sincrono in modo che possa essere determinata esattamente quando viene raggiunto il numero di tagliandi assegnati e basta dare "esaurito" al resto se ce ne sono e, soprattutto, assicurarsi che ogni codice venga servito una sola volta .

Utilizzando temporal, potrebbe essere abbastanza facile creare un lineare, elenco delle attività in ritardo:

var temporal = require("temporal"); 
global.queues = {}; 

app.post('/getCoupon', urlencodedParser, function(req, res){ 
    if (!req.body) return res.status(400).send("Bad Request"); 
    if (!req.body.category) return res.status(200).send("Please Refresh the Page."); 

    // Create global queue at first request or return to it. 
    var queue; 
    if(!global.queues[req.body.objectId]) { 
     queue = global.queues[req.body.objectId] = temporal.queue([]); 
    } 
    else { 
     queue = global.queues[req.body.objectId]; 
    } 

    // Prevent queuing after limit 
    // This will be still concurrent access so in case of large 
    // number of requests a few more may end up queued 
    if(global.given[objectId] >= global.coupons[objectId].length) { 
     res.type('text/plain'); 
     res.status(200).send("Sold out!"); 
     return; 
    } 

    queue.add([{ 
     delay: 200, 
     task: function() { 
     //Go grab a coupon 
     var coupon = getUserACoupon(req.body.objectId); 

     res.type('text/plain'); 
     res.status(200).send(coupon); 

     if(coupon != "Sold Out!" && coupon != "Bad Request: Object does not exist."){ 

      //Update user & product analytics 
      setStatsAfterCouponsSent(req.body.objectId, req.body.sellerProduct, req.body.userEmail, req.body.purchaseProfileId, coupon, req.body.category); 

     } 
     } 
    }]); 
}); 

Un punto chiave qui è che temporale esegue i compiti in sequenza sommando i ritardi, quindi se il ritardo è maggiore è necessario che l'attività venga eseguita, non più di una attività accederà alla matrice di contatore/codici contemporaneamente.

È possibile implementare la propria soluzione basata su questa logica utilizzando l'elaborazione della coda temporizzata, ma il tempo sembra degno di essere provato.

+0

Fantastico! Molte grazie! Adoro la spiegazione e il codice di esempio! – cgauss

+0

Ho dovuto spostare il codice getUserACoupon() tutto per l'esecuzione all'interno dell'attività. Se viene lasciato come mostrato sopra, l'attività non aspetterà che la funzione getUserACoupon() termini, causando problemi. – cgauss

+0

Vedo ... Sì, questo era solo un suggerimento non testato, ma sono contento che ti abbia messo sulla strada giusta. Avevo anche altre riflessioni sulla soluzione proposta, come se per un certo periodo di tempo non ci fossero così tante richieste (come nessuna per il periodo di tempo "delay"), quindi l'elaborazione della coda prende il via e probabilmente è possibile aggiungere di più, quindi la coda deve essere riavviata per lo stesso 'objectId', ma anche questa può essere gestita. – marekful

Problemi correlati