Node/Express:使用会话存储状态时的并发问题

JHH*_*JHH 12 concurrency session node.js express

所以,我已经搜索了很多,发现了几个有点类似的问题,但没有一个真正解决这个问题,所以我认为这应该是一个自己的问题.

我有一个明确的应用程序与一堆路由修改会话以保持状态.事实是,如果有多个并行请求,会话将不时由于请求之间的竞争条件而被覆盖.

通常如此

...
app.use(express.static('/public'));
app.use(session(...));
app.route('methodA').get(function(req, res, next) {
    doSomethingSlow().then(function() {
        req.session.a = 'foo';
        res.send(...);
    }
});
app.route('methodB').get(function(req, res, next) {
    doSomethingElseSlow().then(function() {
        req.session.b = 'bar';
        res.send(...);
    }
});
Run Code Online (Sandbox Code Playgroud)

基本上问题是直截了当的,例如在这个答案中描述.Express将会话存储在res.end()中,但在处理methodA请求时,methodB请求可能同时修改了会话,以便在methodA存储会话时,它将覆盖methodB所做的任何更改.因此,即使node是单线程的,并且所有请求都由同一个线程提供服务,一旦任何方法执行异步操作,我们就会最终出现并发问题,从而同时处理其他请求.

但是,我正在努力决定如何解决这个问题.我发现的所有答案仅列出了最小化发生这种情况的可能性的方法,例如通过在会话MW之前注册服务静态MW来确保静态内容不存储会话.但这只在某种程度上有所帮助; 如果实际上存在应该并行调用的API方法,则需要一些真正的并发方法来进行会话更新(IMO,当涉及并发问题时,每个"解决方案"都力求最小化出现问题的可能性而不是解决问题实际问题肯定会出错).

基本上这些是我目前正在探索的替代方案:

  1. 通过修改我的客户端以确保它以串行方式调用所有API方法,完全防止同一会话中的并行请求.

    这可能是可能的,但会对我的架构产生相当大的影响,并可能影响性能.它也避免了问题,而不是解决它,如果我在我的客户端出错或者API被另一个客户端使用,我可能仍会随机遇到这个问题,所以它感觉不是很强大.

  2. 确保每次会话写入之前都有会话重新加载,并使整个reload-modify-write操作成为原子.

    我不知道如何实现这一目标.即使我修改res.end()重新加载会议刚刚修改并保存它之前,因为读取和写入会话是异步I/O似乎有可能,这可能会发生:

    • 请求A重新加载会话
    • 请求A修改session.A ='foo'
    • 请求B重新加载会话(并且不会看到session.A)
    • 请求A存储会话
    • 请求B修改session.B ='bar'
    • 请求B存储会话,覆盖以前的存储,以便session.A丢失

所以本质上我需要让每个reload-modify-store原子,这基本上意味着阻塞线程?感觉不对,我不知道怎么做.

  1. 完全停止以这种方式使用会话,并将必要的状态作为params传递给每个请求或通过其他方式.

    这也避免了问题而不是解决问题.它也会对我的项目产生巨大影响.但可以肯定的是,这可能是最好的方式.

  2. ???

任何人都有任何想法如何以健壮的方式解决这个问题?谢谢!

Zap*_*ree 1

一个可能的解决方案是为express 创建一个简单的中间件,它将维护所有当前请求的列表并对来自同一会话的任何请求进行排队。在先处理该会话 ID 的先前请求之前,它不会调用 next()。

每次收到请求时,它都可以将其存储在一个对象中,其中键是 session-id cookie 名称,值是当前 session-id 请求的数组。

{ 'session-id1': [... queued requests ...], 'session-id2': ... }

当请求完成时,它将从数组中删除它自己并触发数组中要处理的下一个请求。

您还可以向标头中的每个请求添加一个标志,以允许并发运行不需要排队的请求(例如,它们不写入会话),从而在不需要排队时提高性能。

您应该能够实现这一点,而无需更改您的应用程序。您还可以将标头中的选择退出选项更改为选择加入选项,这意味着除非另有说明,否则它将像平常一样同时处理所有请求。

  • 如果您有多个节点实例和一些数据库会话存储,这会变得更加棘手。在这种情况下,中间件将任何队列保留在内存中并不能解决问题。跨节点实例的中间件也应该通过一些数据库来同步,这使得整体架构更加复杂:-( (2认同)