这个问题本质上与您的last question 非常相似。而且,在阅读我的回复/cmets 之前,您应该阅读该问题的 my answer。
匿名用户之前发布的答案(和见解)相当准确。
只要您有一个高度并发的 (Web) 应用程序/环境,其中有许多不同的同时 HTTP 请求进入,访问同一个 HTTP 会话,总是有可能丢失更新 由竞争的并发 HTTP 请求之间的竞争条件引起。这是由于 Servlet 容器(例如 Apache Tomcat 或 Eclipse Jetty)的本质,因为每个 HTTP 请求都由单独的线程处理并在单独的线程中处理。
不仅 Servlet 容器提供的 HTTP 会话对象需要是线程安全的,您的 Web 应用程序放入 HTTP 会话的所有应用程序域对象也需要是线程安全的。所以,请注意这一点。
此外,大多数 HTTP 会话实现,例如 Apache Tomcat,甚至 Spring Session 的 由不同会话管理提供程序(例如 Spring Session Data Redis 支持的会话实现,或Spring Session Data GemFire)广泛使用“deltas”来仅将更改(或差异)发送到 Session 状态,从而最大限度地减少丢失更新的机会竞争条件。
例如,如果 HTTP 会话当前有一个属性键/值 1/A 并且 HTTP 请求 1(由线程 1 处理)读取 HTTP 会话(只有1/A)并添加一个属性2/B,而另一个并发 HTTP 请求 2(由线程 2 处理)通过会话 ID 读取相同的 HTTP 会话(使用1/A 看到相同的初始会话状态),现在想要添加3/C,那么作为 Web 应用程序开发人员,我们在线程 1 和 2 中的请求 1 和 2 完成后,期望最终结果和 HTTP 会话状态包含属性:[1/A, 2/B, 3/C]。
但是,如果 2 个(甚至更多)竞争 HTTP 请求都在修改 HTTP sessoin 属性 1/A 并且 HTTP 请求/线程 1 想要将属性设置为 1/B 并且竞争 HTTP 请求/线程 2 想要将相同的属性设置为1/C 那么谁赢了?
好吧,事实证明,最后 1 个获胜,或者更确切地说,最后一个写入 HTTP 会话状态的线程获胜,结果可能是 1/B 或 1/C,这是不确定的,并且受调度的变幻莫测、网络延迟、负载等。事实上,几乎不可能推断出哪一个会发生,更不用说总是发生了。
虽然我们的匿名用户提供了一些上下文,例如,一个用户同时使用多个设备(网络浏览器,也可能是移动设备......智能手机或平板电脑),但使用单个用户,甚至多个用户重现此类错误用户不是不可能的,但非常不可能。
但是,如果我们在生产环境中考虑这一点,例如,您可能有数百个 Web 应用程序实例,分布在多台物理机、虚拟机或容器等上,由一些网络负载平衡器进行负载平衡/设备,然后加上当今许多 Web 应用程序是“单页应用程序”、高度复杂的非哑(不再是瘦)但具有 JavaScript 和 AJAX 调用的胖客户端这一事实,然后我们开始了解这种情况更多可能,尤其是在高负载的 Web 应用程序中;想想亚马逊或 Facebook。考虑到 Web 应用程序可以进行的所有动态异步调用,不仅有许多并发用户,而且单个用户的许多并发请求。
不过,正如我们的匿名用户所指出的,这并不能成为 Web 应用程序开发人员负责任地设计和编码我们的 Web 应用程序的借口。
一般来说,我会说 HTTP 会话应该只用于跟踪非常少的(即数量)和必要的信息,以保持良好的用户体验并在用户转换时保持用户和应用程序之间的正确交互Web 应用程序的不同部分或阶段,例如跟踪偏好或项目(在购物车中)。通常,不应使用 HTTP 会话来存储“事务性”数据。这样做会让自己陷入困境。 HTTP 会话应该主要是一个读取繁重的数据结构(而不是写入繁重),特别是因为 HTTP 会话可以并且很可能会被多个线程访问。
当然,不同的后备数据存储(如 Redis,甚至 GemFire)都提供锁定机制。 GemFire 甚至提供缓存级别的事务,这在处理由 HTTP 会话对象管理的 Web 交互时非常繁重且不合适(不要与事务混淆)。甚至锁定也会给应用程序带来严重的争用和延迟。
无论如何,所有这些都是说您非常需要注意交互和数据访问模式,否则您会发现自己陷入困境,所以要小心,永远!
值得深思!