【问题标题】:connect-redis - how to protect the session object against race conditionconnect-redis - 如何保护会话对象免受竞争条件的影响
【发布时间】:2012-07-11 06:48:18
【问题描述】:

我正在使用 nodejs 和 connect-redis 来存储会话数据。

我在会话中保存用户数据,并在会话生命周期中使用它。

我注意到两个请求之间可能存在竞争条件,从而更改会话数据。

我尝试过使用 redis-lock 来锁定会话,但对我来说有点问题。

我不想锁定整个会话,而是只锁定特定的会话变量。

我发现这是不可能的,我想了解决它的方向:

停止使用session对象存储用户数据,直接将变量保存在redis中并在使用前加锁。

我知道它可以工作,但它需要我手动管理所有对象,而不是仅仅通过会话对象访问 redis。

您能否与我分享最佳实践和您的建议?

谢谢, 利奥

【问题讨论】:

    标签: node.js redis express race-condition


    【解决方案1】:

    嗯,实施您自己的存储可能是您的选择。 This documentation 表明你只需要实现三个方法:.get.set.destroy(见最后一段)。应该是这样的(使用node-redis library 并稍微修改the original connect-redis store):

    var redis = require("redis"),
        redis_client = redis.createClient(),
        session_prefix = 'session::',
        lock_suffix = '::lock',
        threshold = 5000,
        wait_time = 250,
        oneDay = 86400;
    
    /* If timeout is greater then threshold, then we assume that
       one of the Redis Clients is dead and he cannot realese
       the lock. */
    
    function CustomSessionStore(opts) {
        opts = opts || {};
        var self = this;
        self.ttl = opts.ttl; // <---- used for setting timeout on session
    
        self.lock = function(sid, callback) {
            callback = callback || function(){};
            var key = session_prefix + sid + lock_suffix;
            // try setting the lock with current Date
            redis_client.setnx(key, Date.now( ), function(err, res) {
                // some error handling?
                if (res) {
                    // Everything's fine, call callback.
                    callback();
                    return;
                }
    
                // setnx failed, look at timeout
                redis_client.get(key, function(err, res) {
                    // some error handling?
                    if (parseInt(res) + threshold > Date.now( )) {
                        // timeout, release the old lock and lock it
                        redis_client.getset(key, Date.now( ), function(err, date) {
                            if (parseInt(date) + threshold > Date.now()) {
                                // ups, some one else was faster in acquiring lock
                                setTimeout(function() {
                                    self.lock(sid, callback);
                                }, wait_time);
                                return;
                            }
                            callback();
                        });
                        return;
                    }
                    // it is not time yet, wait and try again later
                    setTimeout(function() {
                        self.lock(sid, callback);
                    }, wait_time);
                });
            });
        };
    
        self.unlock = function(sid, callback) {
            callback = callback || function(){};
            var key = session_prefix + sid + lock_suffix;
            redis_client.del(key, function(err) {
                // some error handling?
                callback();
            });
        };
    
        self.get = function(sid, callback) {
            callback = callback || function(){};
            var key = session_prefix + sid;
            // lock the session
            self.lock(sid, function() {
                redis_client.get(key, function(err, data) {
                    if (err) {
                        callback(err);
                        return;
                    }
                    try {
                        callback(null, JSON.parse(data));
                    } catch(e) {
                        callback(e);
                    }
                });
            });
        };
    
        self.set = function(sid, data, callback) {
            callback = callback || function(){};
            try {
                // ttl used for expiration of session
                var maxAge = sess.cookie.maxAge
                  , ttl = self.ttl
                  , sess = JSON.stringify(sess);
    
                ttl = ttl || ('number' == typeof maxAge
                      ? maxAge / 1000 | 0
                      : oneDay);
    
            } catch(e) {
                callback(e);
                return;
            }
            var key = session_prefix + sid;
            redis_client.setex(key, ttl, data, function(err) {
                // unlock the session
                self.unlock(sid, function(_err) {
                    callback(err || _err);
                });
            });
        };
    
        self.destroy = function(sid, callback) {
            var key = session_prefix + sid;
            redis_client.del(key, function(err) {
                redis_client.unlock(sid, function(_err) {
                    callback(err || _err);
                });
            });
        };
    }
    

    旁注:我没有为.lock.unlock 实现错误处理。我把这个留给你! :) 可能会有一些小错误(我现在没有 NodeJS,我是凭记忆写的 :D),但你应该理解这个想法。这是the link,其中包含有关如何使用setnx 锁定/解锁Redis 的讨论。

    另一个注意事项:您可能希望对路由进行一些自定义错误处理,因为如果任何路由抛出异常,那么 Redis 会话将不会被解锁。 .set 方法总是作为路由中的最后一件事调用 - 与 Express 在路由开始时调用的 .get 方法相反(这就是我锁定 .get 并在 .set 解锁的原因)。您仍然只会被锁定 5 秒钟,因此这不是问题。请记住根据您的需要调整它(尤其是 thresholdwait_time 变量)。

    最后说明:使用这种机制,您的请求处理程序只会为每个用户一个接一个地触发。这意味着,您将无法为每个用户运行并发处理程序。这可能是一个问题,因此另一个想法是将数据保存在会话之外并手动处理锁定/解锁。毕竟,有些事情必须手动处理。

    希望对你有帮助!祝你好运!

    【讨论】:

    • 嗨!感谢您非常详细的解释!我已经使用 2 个中间件成功锁定了会话(1 个在会话创建之前,一个在请求完成之后)。如果我想在用户会话中支持并发,我想手动方法是最好的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-30
    • 1970-01-01
    • 2022-01-26
    • 1970-01-01
    • 1970-01-01
    • 2022-07-15
    相关资源
    最近更新 更多