如果可能有多个服务器(并且每个服务器都可以有多个线程),如何处理竞争条件
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
这里我们需要考虑两件事:
- 确保
Quantity
永远不会为负数
- 如果我们同时请求一个产品,我们必须确保有效
Quantity
。
换句话说,我们可以有:
request1
5 台笔记本电脑(此请求将在 thread1
处理)
request2
1 台笔记本电脑(此请求将在 thread2
处理)
当两个请求都被处理后,数据库应该包含
Id
Name
Quantity
1
Laptop
4
但是,情况可能并非如此,具体取决于我们编写代码的方式。
如果在我们的服务器上有类似这样的东西:
var product = _database.GetProduct();
if (product.Quantity - requestedQuantity >= 0)
{
product.Quantity -= requestedQuantity;
_database.Save();
}
使用此代码,两个请求(在不同的线程上执行)可能会同时到达代码的第一行。
thread1
: _database.GetProduct(); // 数量为 10
thread2
: _database.GetProduct(); // 数量为 10
thread1
: _product.Quantity = 10 - 5 = 5
thread2
: _product.Quantity = 10 - 1 = 9
thread1
: _database.Save(); // 数量为 5
thread2
: _database.Save(); // 数量为 9
刚刚发生了什么?我们已经售出 6 台笔记本电脑,但我们只从库存中减少了一台。
如何解决这个问题?
为了确保只有正数,我们可以使用一些 DB 约束(模仿 unsigned int)。
为了处理竞争条件,我们通常使用 lock
和类似的技术。
根据可能有效的情况,如果我们有一个服务器实例...但是,当我们有多个服务器实例并且服务器在多线程上 运行 时我们应该做什么环境?
在我看来,当您拥有多个 Web 服务器时,唯一合理的 锁定选项就是数据库。为什么我说合理?因为我们有 Mutex
.
一个lock
只允许一个线程进入被锁定的部分,并且该锁不与任何其他进程共享。
A mutex
与锁相同,但它可以是系统范围的(由多个进程共享)。
现在......这是我的个人意见,但我希望在面向微服务的世界中管理 Mutex
几个进程,在这个世界中,服务器的新实例可以每秒旋转一次,或者现有的服务器实例每秒都可能死亡是棘手和混乱的(我们有一些 Github 示例吗?)。
那怎么解决问题呢?
- 存储过程* - 将责任卸载到数据库。编写一个新的存储过程并将整个逻辑包装到一个事务中。每个服务器都会调用这个 SP,我们不需要担心任何事情。但这可能会很慢?
- SELECT ...更新 - 我在调查问题时看到了这个。使用这种方法,我们仍然尝试在 'database' 级别上解决问题。
考虑到以上所有情况,解决此问题的最佳方法应该是什么?我还缺少其他解决方案吗?你有什么建议?
我在 .NET 中工作并在 PostgreSQL 中使用 EF Core,但我认为这确实是一个与语言无关的问题,解决该问题的原则在所有环境中都是相似的(并且对于许多环境也是相似的 关系数据库)。
阅读大部分评论后,我们假设您需要一个 关系数据库.
的解决方案
您需要保证的主要事情是,只有前提条件仍然有效(例如product.Quantity - requestedQuantity
),代码末尾的写操作才会发生。
此先决条件在内存中的应用程序端进行评估。但是应用程序目前只能看到数据的快照,当数据库读取发生时:_database.GetProduct();
一旦其他人更新相同的数据,这可能会过时。如果您想避免使用 SERIALIZABLE
作为事务隔离级别(无论如何都会影响性能),应用程序应该在写入时检测前提条件是否仍然有效。或者换句话说,如果数据在处理数据时未更改。
这可以通过使用离线并发模式来完成:optimistic offline lock or a pessimistic offline lock。许多 ORM 框架默认支持这些功能。
假设我们有一个库存系统,用于跟踪商店中的可用产品数量(数量)。所以我们可以有类似的东西:
Id | Name | Quantity |
---|---|---|
1 | Laptop | 10 |
这里我们需要考虑两件事:
- 确保
Quantity
永远不会为负数 - 如果我们同时请求一个产品,我们必须确保有效
Quantity
。
换句话说,我们可以有:
request1
5 台笔记本电脑(此请求将在thread1
处理)request2
1 台笔记本电脑(此请求将在thread2
处理)
当两个请求都被处理后,数据库应该包含
Id | Name | Quantity |
---|---|---|
1 | Laptop | 4 |
但是,情况可能并非如此,具体取决于我们编写代码的方式。 如果在我们的服务器上有类似这样的东西:
var product = _database.GetProduct();
if (product.Quantity - requestedQuantity >= 0)
{
product.Quantity -= requestedQuantity;
_database.Save();
}
使用此代码,两个请求(在不同的线程上执行)可能会同时到达代码的第一行。
thread1
: _database.GetProduct(); // 数量为 10thread2
: _database.GetProduct(); // 数量为 10thread1
: _product.Quantity = 10 - 5 = 5thread2
: _product.Quantity = 10 - 1 = 9thread1
: _database.Save(); // 数量为 5thread2
: _database.Save(); // 数量为 9
刚刚发生了什么?我们已经售出 6 台笔记本电脑,但我们只从库存中减少了一台。
如何解决这个问题?
为了确保只有正数,我们可以使用一些 DB 约束(模仿 unsigned int)。
为了处理竞争条件,我们通常使用 lock
和类似的技术。
根据可能有效的情况,如果我们有一个服务器实例...但是,当我们有多个服务器实例并且服务器在多线程上 运行 时我们应该做什么环境?
在我看来,当您拥有多个 Web 服务器时,唯一合理的 锁定选项就是数据库。为什么我说合理?因为我们有 Mutex
.
一个lock
只允许一个线程进入被锁定的部分,并且该锁不与任何其他进程共享。
A mutex
与锁相同,但它可以是系统范围的(由多个进程共享)。
现在......这是我的个人意见,但我希望在面向微服务的世界中管理 Mutex
几个进程,在这个世界中,服务器的新实例可以每秒旋转一次,或者现有的服务器实例每秒都可能死亡是棘手和混乱的(我们有一些 Github 示例吗?)。
那怎么解决问题呢?
- 存储过程* - 将责任卸载到数据库。编写一个新的存储过程并将整个逻辑包装到一个事务中。每个服务器都会调用这个 SP,我们不需要担心任何事情。但这可能会很慢?
- SELECT ...更新 - 我在调查问题时看到了这个。使用这种方法,我们仍然尝试在 'database' 级别上解决问题。
考虑到以上所有情况,解决此问题的最佳方法应该是什么?我还缺少其他解决方案吗?你有什么建议?
我在 .NET 中工作并在 PostgreSQL 中使用 EF Core,但我认为这确实是一个与语言无关的问题,解决该问题的原则在所有环境中都是相似的(并且对于许多环境也是相似的 关系数据库)。
阅读大部分评论后,我们假设您需要一个 关系数据库.
的解决方案您需要保证的主要事情是,只有前提条件仍然有效(例如product.Quantity - requestedQuantity
),代码末尾的写操作才会发生。
此先决条件在内存中的应用程序端进行评估。但是应用程序目前只能看到数据的快照,当数据库读取发生时:_database.GetProduct();
一旦其他人更新相同的数据,这可能会过时。如果您想避免使用 SERIALIZABLE
作为事务隔离级别(无论如何都会影响性能),应用程序应该在写入时检测前提条件是否仍然有效。或者换句话说,如果数据在处理数据时未更改。
这可以通过使用离线并发模式来完成:optimistic offline lock or a pessimistic offline lock。许多 ORM 框架默认支持这些功能。