如果可能有多个服务器(并且每个服务器都可以有多个线程),如何处理竞争条件

How to deal with race condition in case when it's possible to have multiple servers (and each of them can have multiple threads)

假设我们有一个库存系统,用于跟踪商店中的可用产品数量(数量)。所以我们可以有类似的东西:

Id Name Quantity
1 Laptop 10

这里我们需要考虑两件事:

  1. 确保 Quantity 永远不会为负数
  2. 如果我们同时请求一个产品,我们必须确保有效 Quantity

换句话说,我们可以有:

当两个请求都被处理后,数据库应该包含

Id Name Quantity
1 Laptop 4

但是,情况可能并非如此,具体取决于我们编写代码的方式。 如果在我们的服务器上有类似这样的东西:

var product = _database.GetProduct();
if (product.Quantity - requestedQuantity >= 0)
{
   product.Quantity -= requestedQuantity;
   _database.Save();
}

使用此代码,两个请求(在不同的线程上执行)可能会同时到达代码的第一行。

  1. thread1: _database.GetProduct(); // 数量为 10
  2. thread2: _database.GetProduct(); // 数量为 10
  3. thread1: _product.Quantity = 10 - 5 = 5
  4. thread2: _product.Quantity = 10 - 1 = 9
  5. thread1: _database.Save(); // 数量为 5
  6. thread2: _database.Save(); // 数量为 9

刚刚发生了什么?我们已经售出 6 台笔记本电脑,但我们只从库存中减少了一台。

如何解决这个问题?

为了确保只有正数,我们可以使用一些 DB 约束(模仿 unsigned int)。

为了处理竞争条件,我们通常使用 lock 和类似的技术。 根据可能有效的情况,如果我们有一个服务器实例...但是,当我们有多个服务器实例并且服务器在多线程上 运行 时我们应该做什么环境?

在我看来,当您拥有多个 Web 服务器时,唯一合理的 锁定选项就是数据库。为什么我说合理?因为我们有 Mutex.

一个lock只允许一个线程进入被锁定的部分,并且该锁不与任何其他进程共享。

A mutex 与锁相同,但它可以是系统范围的(由多个进程共享)。

现在......这是我的个人意见,但我希望在面向微服务的世界中管理 Mutex 几个进程,在这个世界中,服务器的新实例可以每秒旋转一次,或者现有的服务器实例每秒都可能死亡是棘手和混乱的(我们有一些 Github 示例吗?)。

那怎么解决问题呢?

  1. 存储过程* - 将责任卸载到数据库。编写一个新的存储过程并将整个逻辑包装到一个事务中。每个服务器都会调用这个 SP,我们不需要担心任何事情。但这可能会很慢?
  2. SELECT ...更新 - 我在调查问题时看到了这个。使用这种方法,我们仍然尝试在 'database' 级别上解决问题。

考虑到以上所有情况,解决此问题的最佳方法应该是什么?我还缺少其他解决方案吗?你有什么建议?

我在 .NET 中工作并在 PostgreSQL 中使用 EF Core,但我认为这确实是一个与语言无关的问题,解决该问题的原则在所有环境中都是相似的(并且对于许多环境也是相似的 关系数据库)。

阅读大部分评论后,我们假设您需要一个 关系数据库.

的解决方案

您需要保证的主要事情是,只有前提条件仍然有效(例如product.Quantity - requestedQuantity),代码末尾的写操作才会发生。

此先决条件在内存中的应用程序端进行评估。但是应用程序目前只能看到数据的快照,当数据库读取发生时:_database.GetProduct(); 一旦其他人更新相同的数据,这可能会过时。如果您想避免使用 SERIALIZABLE 作为事务隔离级别(无论如何都会影响性能),应用程序应该在写入时检测前提条件是否仍然有效。或者换句话说,如果数据在处理数据时未更改。

这可以通过使用离线并发模式来完成:optimistic offline lock or a pessimistic offline lock。许多 ORM 框架默认支持这些功能。