如何使用Node.js以70个请求/秒分发唯一的优惠券代码

cga*_*uss 9 javascript ajax object node.js

当我们启动交易时,我会运行一个可以看到50-70个请求/秒的优惠券网站(我们每天多次推出20多个交易).当交易上线时,我们的用户按下按钮即可获得特定产品的优惠券,该优惠券通过ajax https请求提供独特的优惠券代码.每张优惠券只能兑换一次.

我的问题是,在这些时候如此高的流量,可以将相同的优惠券分发给多个用户.这是不好的,因为只有其中一个人能够实际兑换优惠券,留给另一方的糟糕用户体验.

我将所有优惠券信息存储在IBM Bluemix托管的node.js服务器上的内存中的对象中.我想这可以让我快速处理请求.

我如何存储优惠券信息:

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;
}
Run Code Online (Sandbox Code Playgroud)

处理优惠券要求:

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;
}
Run Code Online (Sandbox Code Playgroud)

我对node.js服务器及其运行方式没有太多了解.

一如既往,感谢您的帮助!

mar*_*ful 5

问题在于Node的非阻塞/异步特性.从同时请求调用相同的函数不会等待彼此完成.许多请求进入并同时访问全局代码数组.

您多次发出相同的代码,因为计数器会同时增加多个请求,因此可能会发生多个请求看到相同的计数器状态.

管理并发问题的一种方法是一次只允许一次访问(getUserACoupon在您的情况下),因此使用优惠券的执行部分是同步的互斥的.实现此目的的一种方法是锁定机制,因此当一个请求获得对锁的访问权时,进一步的请求将等待直到锁被释放.在伪代码中,它看起来像这样:

wait until lock exists
create lock
if any left, consume one coupon
remove lock
Run Code Online (Sandbox Code Playgroud)

但是这种方法违背了Node的非阻塞性质,并且还引入了在多个请求等待时释放时获取锁定的问题.

更好的方法是更有可能是队列系统.它应该工作,因此代码不会在请求时被消耗,而是作为可调用的方式放入队列中,等待启动.您可以读取队列的长度并停止接受新请求("售罄"),但是,这仍然会在全局队列/计数器上并发,因此您最终可能会获得比优惠券更多的排队项目,但这是这不是问题,因为队列将被同步处理,因此可以准确地确定何时达到分配的优惠券的数量,并且如果有的话,只是给其余部分"售罄",更重要的是,确保每个代码仅被提供一次.

使用temporal,可以很容易地创建线性,延迟的任务列表:

var temporal = require("temporal");
global.queues = {};
Run Code Online (Sandbox Code Playgroud)
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);

        }
      }  
    }]);
});
Run Code Online (Sandbox Code Playgroud)

这里的一个关键点是temporal执行任务顺序累加延迟,因此如果延迟更多则需要运行任务,则一次只能访问一个计数器/代码数组.

您可以使用定时队列处理基于此逻辑实现您自己的解决方案,但是时间似乎值得一试.