2012-07-10 54 views
2

我正在使用nodejs和connect-redis來存儲會話數據。connect-redis - 如何保護會話對象免受競爭條件

我在會話中保存用戶數據,並在會話生存期中使用它。

我注意到兩個請求之間有競爭條件可能會改變會話數據。

我試着用redis-lock來鎖定會話,但對我來說有點問題。

我不想鎖定整個會話,而是隻鎖定特定的會話變量。

我發現它是不可能的,我想到方向來解決它:

停止使用會話對象來存儲用戶數據,並且直接保存變量在redis的和使用它之前鎖定。

我知道它可以工作,但它需要我手動管理所有對象,而不是通過會話對象訪問redis。

您能否與我分享最佳實踐和您的建議?

感謝, 利奧爾

回答

4

好,實現自己的存儲可能是你的選擇。 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變量)。

最後注意事項:有了這個機制,你的請求處理程序將只會爲每個用戶一個接一個地觸發。這意味着,你會不能以爲每個用戶運行併發處理程序。這可能是一個問題,所以另一個想法是在會話之外保存數據並手動處理鎖定/解鎖。畢竟,有些東西需要手動處理。

我希望它有幫助!祝你好運!

+0

嗨!感謝您的非常詳細的解釋!我已經成功地使用2箇中間件鎖定了會話(在創建會話之前和請求完成之後)。我想如果我想要支持用戶會話中的併發性,手動方法將是最好的。 – liorix 2012-07-11 11:35:08