2015-04-17 14 views
9

Prowadzę stronę z kuponami, która widzi 50-70 żądań/sekundę, gdy uruchamiamy nasze oferty (uruchamiamy ponad 20 transakcji jednocześnie kilka razy dziennie). Gdy transakcje zostaną opublikowane, nasi użytkownicy naciskają przycisk, aby odebrać kupon na konkretny produkt, który wyświetla unikalny kod kuponu za pomocą żądania ajax https. Każdy kupon można wykorzystać tylko raz.Jak rozpowszechniać unikalne kody kuponów z 70 żądaniami na sekundę przy użyciu Node.js

Mój problem polega na tym, że przy tak dużym natężeniu ruchu w tym czasie ten sam kupon może być dystrybuowany do wielu użytkowników. Jest to złe, ponieważ tylko jeden z nich będzie w stanie zrealizować kupon pozostawiając słabe wrażenia użytkownika dla drugiego.

Przechowuję wszystkie informacje o kuponach w obiektach w pamięci na serwerze node.js obsługiwanym przez IBM Bluemix. Pomyślałem, że to pozwoli mi szybko obsłużyć wnioski.

Jak przechowywać informacje Kupon:

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

Uchwyt kupon żądanie:

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

nie mam mnóstwo zrozumienia serwerów node.js i jak funkcjonują.

Jak zawsze, dziękuję za pomoc!

+0

Node.js ma tylko jeden wątek, więc generowanie niepowtarzalnych kuponów nie powinno stanowić problemu, ponieważ różni się od poprzednich wartości. Problem polegałby na szybkim porównaniu, ponieważ skumulowane przeszłe kupony rosłyby z czasem. Tylko spitballing tutaj, ale myślę, że aktualizowanie Hash z nową wartością za każdym razem powinno zagwarantować wyjątkowość nowszego hasha? – laggingreflex

+0

@laggingreflex Kupony nie są generowane na serwerze. Kupony są generowane przez Amazon, a następnie przechowywane w tablicy w mojej bazie danych. Problem polega tylko na upewnieniu się, że łapię kupon z obiektu global.coupons tylko raz. Powodem, dla którego zacząłem używać Node.js, było to, że jest to pojedynczy wątek, więc pomyślałem, że nie napotkam na ten problem, ale udowodniono mi, że nie. – cgauss

Odpowiedz

5

Problem leży w nieblokującym/asynchronicznym charakterze węzła. Wywołania tej samej funkcji z jednoczesnych żądań nie czekają na siebie nawzajem, aby zakończyć. Wiele żądań przychodzi i jednocześnie uzyskuje dostęp do tablicy kodów globalnych.

Podaje się ten sam kod wiele razy, ponieważ counter is incremented by multiple requests concurrently, więc może się zdarzyć, że więcej niż jeden wniosek widzi ten sam stan licznika.

Jedno podejście do zarządzania problem współbieżności jest, aby umożliwić dostęp tylko jeden (do getUserACoupon w danym przypadku) na raz, tak że część z realizacją gdzie kupon jest zużywana jest zsynchronizowane lub wzajemnie się wykluczają. Jednym ze sposobów osiągnięcia tego jest mechanizm blokujący, więc gdy jedno żądanie uzyskuje dostęp do zamka, kolejne żądania czekają aż do zwolnienia blokady. W pseudo kod mógłby wyglądać następująco:

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

Ale takie podejście jest sprzeczne z non-blocking charakter węzła, a także wprowadza się z problemem, który staje po zwolnieniu blokady, jeśli więcej niż jeden wniosek czekał.

Lepszym sposobem jest bardziej prawdopodobny system kolejki. Powinno zadziałać tak, aby kod nie został zużyty w czasie żądania, ale został umieszczony w kolejce jako żądanie wywołania, czekając na rozpoczęcie. Możesz odczytać długość kolejki i zaprzestać przyjmowania nowych żądań ("wyprzedane"), jednak będzie to nadal współbieżne przez globalną kolejkę/licznik, dzięki czemu możesz otrzymać jeszcze kilka pozycji w kolejce niż kupony, ale to jest nie stanowi problemu, ponieważ kolejka będzie przetwarzana synchronicznie, dzięki czemu można ją dokładnie określić, kiedy liczba przydzielonych kuponów zostanie osiągnięta, i po prostu dać "wyprzedane" resztę, jeśli taka jest, i, co ważniejsze, zapewnić, że każdy kod jest serwowany tylko raz .

Korzystanie temporal, może to być dość łatwe do tworzenia liniowy, opóźnione lista zadań:

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

     } 
     } 
    }]); 
}); 

Kluczowym punktem jest to, że czasowy wykonuje zadania kolejno sumując opóźnienia, więc jeśli opóźnienie jest większe niż wymagane, aby zadanie zostało uruchomione, nie więcej niż jedno zadanie uzyska dostęp do tablicy licznika/kodów naraz.

Można również zaimplementować własne rozwiązanie oparte na tej logice, również za pomocą przetwarzania kolejki czasu, ale warto wypróbować czasowo.

+0

Awesome! Wielkie dzięki! Uwielbiam wyjaśnienie i przykładowy kod! – cgauss

+0

Musiałem przenieść kod getUserACoupon(), aby uruchomić go w ramach zadania. Jeśli pozostanie tak, jak pokazano powyżej, zadanie nie będzie czekać na zakończenie funkcji getUserACoupon(), co spowoduje problemy. – cgauss

+0

Rozumiem ... Tak, to była tylko nie sprawdzona podpowiedź, ale cieszę się, że postawiła cię na właściwej drodze. Miałem też trochę więcej przemyśleń na temat proponowanego rozwiązania, jak gdyby przez pewien czas nie było zbyt wielu żądań (jak żadne nie przychodzi na czas 'opóźnienia '), to przetwarzanie kolejki rozpoczyna się i prawdopodobnie możesz nie dodaje więcej, więc kolejka musi zostać zrestartowana dla tego samego obiektu 'objectId', ale można to również obsłużyć. – marekful