2015-04-17 2 views
9

Ich betreibe eine Gutscheinseite mit 50-70 Anfragen pro Sekunde, wenn wir unsere Angebote starten (wir starten mehr als 20 Angebote gleichzeitig mehrmals am Tag). Wenn die Deals live gehen, drücken unsere Nutzer einen Button, um einen Coupon für ein bestimmtes Produkt zu erhalten, der über eine Ajax-https-Anfrage einen einzigartigen Couponcode anbietet. Jeder Gutschein kann nur einmal eingelöst werden.So verteilen Sie einzigartige Gutscheincodes mit 70 Anfragen/Sek. Mit Node.js

Mein Problem ist, dass mit so hohen Verkehrsaufkommen zu diesen Zeiten der gleiche Gutschein an mehrere Benutzer verteilt werden kann. Dies ist schlecht, da nur einer von ihnen in der Lage sein wird, den Coupon tatsächlich einzulösen, was zu einer schlechten Benutzererfahrung für den anderen führt.

Ich speichere alle Couponinformationen in Objekten im Speicher auf einem node.js-Server, der von IBM Bluemix gehostet wird. Ich dachte, dies würde mir erlauben, die Anfragen schnell zu bearbeiten.

Wie ich den Gutschein Informationen speichern:

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; 
} 

Griff Coupon anfordern:

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; 
} 

Ich habe nicht eine Tonne Verständnis von node.js Server und wie sie funktionieren.

Wie immer, danke für die Hilfe!

+0

Node.js ist nur single-threaded, daher sollte es kein Problem sein, eindeutige Coupons zu generieren, um sicherzustellen, dass sie sich von den bisherigen Werten unterscheiden. Das Problem wäre, diesen Vergleich schnell zu machen, da die angesammelten vergangenen Coupons nur mit der Zeit wachsen würden. Ich spucke nur hier, aber ich denke, dass das Aktualisieren eines Hash mit dem neuen Wert jedes Mal die Einzigartigkeit des neueren Hashes garantieren sollte? – laggingreflex

+0

@laggingreflex Die Coupons werden nicht auf dem Server generiert. Die Gutscheine werden von Amazon generiert und dann in einem Array in meiner Datenbank gespeichert. Das Problem besteht einfach darin, sicherzustellen, dass ich einen Coupon nur einmal aus dem global.coupons-Objekt ergreife. Der Grund warum ich anfing Node.js zu benutzen war, weil es single threaded ist, also dachte ich, dass ich nicht auf dieses Problem stoßen würde, aber ich habe mich als falsch erwiesen. – cgauss

Antwort

5

Das Problem liegt in der nicht blockierenden/asynchronen Natur von Node. Aufrufe derselben Funktion von gleichzeitigen Anforderungen warten nicht aufeinander ab. Viele Anfragen kommen und gleichzeitig Zugriff auf die globale Codes-Array.

Sie geben den gleichen Code mehrere Male, weil die counter is incremented by multiple requests concurrently so kann es passieren, dass mehr als eine Anfragen den gleichen Zählerstand sieht.

Ein Ansatz, die Gleichzeitigkeit Problem zu verwalten, ist nur ein Zugang zu ermöglichen (zu getUserACoupon in Ihrem Fall) zu einer Zeit, so dass ein Teil der Ausführung, wo ein Gutschein verbraucht ist synchronisiert oder sie gegenseitig ausschließende. Eine Möglichkeit, dies zu erreichen, ist ein Verriegelungsmechanismus. Wenn also eine Anforderung Zugriff auf die Sperre erhält, warten weitere Anforderungen, bis die Sperre aufgehoben wird. In Pseudo-Code könnte es etwa so aussehen:

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

Aber dieser Ansatz geht gegen die nicht-blockierende Natur von Knoten und führt auch das Problem, wer die Sperre erhält, wenn freigegeben, wenn mehr als eine Anforderung wartet.

Ein besserer Weg ist eher ein Warteschlangensystem. Es sollte so funktionieren ein Code wird nicht zum Zeitpunkt der Anforderung verbraucht, sondern in einer Warteschlange als eine abrufbar gestellt, warten auf den Anstoß. Sie können die Länge der Warteschlange lesen und keine neuen Anfragen mehr akzeptieren ("ausverkauft"). Dies ist jedoch immer noch gleichzeitig über eine globale Warteschlange/einen globalen Zähler, so dass Sie möglicherweise einige mehr in der Warteschlange befindliche Elemente als Gutscheine haben kein Problem, weil die Warteschlange synchron verarbeitet wird so kann genau bestimmt werden, wenn die Anzahl der zugewiesenen Coupons erreicht werden und nur "ausverkauft" an den Rest geben, wenn überhaupt und, noch wichtiger, sicherstellen, dass jeder Code nur einmal serviert wird .

Mit temporal, könnte es ganz einfach sein, eine linear zu erstellen, verzögerte Aufgabenliste:

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); 

     } 
     } 
    }]); 
}); 

Ein wichtiger Punkt ist hier, dass Zeitlichen die Aufgaben nacheinander Addieren der Verzögerungen führt, Wenn also die Verzögerung größer ist als für die Ausführung der Aufgabe, greift nicht mehr als eine Aufgabe gleichzeitig auf das Counter/Code-Array zu.

Sie könnten Ihre eigene Lösung basierend auf dieser Logik auch mit zeitgesteuerter Queue-Verarbeitung implementieren, aber zeitlich scheint einen Versuch wert.

+0

Super! Danke vielmals! Liebe die Erklärung und Beispielcode! – cgauss

+0

Ich musste den getUserACoupon() -Code alle verschieben, um innerhalb der Aufgabe zu laufen. Wenn es wie oben gezeigt belassen wird, wartet die Task nicht auf das Beenden der Funktion getUserACoupon(), was zu Problemen führte. – cgauss

+0

Ich verstehe ... Ja, das war nur ein unerforschter Hinweis, aber ich bin froh, dass es dich auf den richtigen Weg gebracht hat. Ich hatte auch einige Gedanken über die vorgeschlagene Lösung, als ob für eine Zeitperiode nicht so viele Anfragen (wie keine kommt für die "Verzögerung" Zeit), dann beginnt die Warteschlange Verarbeitung und wahrscheinlich können Sie fügen Sie nicht mehr hinzu, also muss die Warteschlange für die gleiche 'objectId' neu gestartet werden, aber das kann auch gehandhabt werden. – marekful

Verwandte Themen