如果可能有多个服务器(并且每个服务器都可以有多个线程),如何处理竞争条件
Posted
技术标签:
【中文标题】如果可能有多个服务器(并且每个服务器都可以有多个线程),如何处理竞争条件【英文标题】:How to deal with race condition in case when it's possible to have multiple servers (and each of them can have multiple threads) 【发布时间】:2021-12-25 23:28:02 【问题描述】:假设我们有一个库存系统来跟踪商店中可用的产品数量(数量)。所以我们可以有类似的东西:
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
和类似的技术。
并且根据可能的情况,如果我们有一个服务器实例......但是,当我们有多个服务器实例并且服务器运行在多线程环境中时,我们应该怎么做?
在我看来,当您拥有多个网络服务器时,您唯一合理 的锁定选项就是数据库。为什么我说合理?因为我们有Mutex
。
lock
只允许一个线程进入被锁定的部分,并且该锁不与任何其他进程共享。
mutex
与锁相同,但它可以是系统范围的(由多个进程共享)。
现在...这是我的个人意见,但我希望在面向微服务的世界中的几个进程之间管理Mutex
,其中服务器的新实例每秒可以启动,或者服务器的现有实例can die per second 是棘手和混乱的(我们有一些 Github 的例子吗?)。
那该如何解决呢?
-
存储过程* - 将责任转移到数据库。编写一个新的存储过程并将整个逻辑包装到一个事务中。每个服务器都会调用这个SP,我们什么都不用担心。但这可能会很慢?
SELECT ...FOR UPDATE - 我在调查问题时看到了这一点。通过这种方法,我们仍然尝试在“数据库”级别解决问题。
考虑到以上所有因素,解决此问题的最佳方法应该是什么?我还缺少其他解决方案吗?你有什么建议?
我在 .NET 中工作并将 EF Core 与 PostgreSQL 一起使用,但我认为这确实是一个与语言无关的问题,解决问题的原则在所有环境中都是相似的(对于许多关系 数据库)。
【问题讨论】:
我使用了 SELECT FOR UPDATE 并且之前它就像魔术一样工作。我把它放在一个交易中,但我没必要这样做。 相关问题:***.com/q/10935850/217324 我很想投票结束这个问题,因为你花了一半以上的文字来解释“竞争条件”的含义。任何有资格帮助您的人都已经知道“比赛条件”是什么意思。不关闭,因为我不是数据库专家。但我有一个自己的“愚蠢问题”。数据库可能是分布式的,但您的代码示例的编写就像应用程序与它有一个连接一样。那么,为什么您不能将整个示例包装在一个事务中,然后重试,直到 (A) 事务成功,或 (B) 其他人抢走了所有笔记本电脑? 我不是数据库专家,但如果交易没有完全阻止您试图阻止的事情,那么交易的意义何在?我认为打开事务会在一个瞬间生成数据库的快照。当您与“数据库”交互时,您实际上与快照交互。然后,当您尝试提交时,您的所有更改都有效地在一个瞬间发生,或者提交失败,因为其他人在您之前提交,并且他们的提交影响了记录(包括您仅获取的记录)你的提交取决于。我错了吗? @SolomonSlow 您在上一条评论中描述的内容实际上可以通过 SERIALIZABLE 隔离级别实现。但是,没有数据库(据我所知)默认使用此隔离级别,即使您手动启用它也会带来一些后果。我仍然想听听那些有机会在现实世界的应用程序中使用这样的东西的人的意见。似乎现在我有 3 种不同的方法来解决这个问题,但仍然不确定是否有任何“更聪明”或更“简单”的方法来处理这个问题。 【参考方案1】:在阅读了大部分 cmets 之后,让我们假设您需要一个关系数据库的解决方案。
您需要保证的主要事情是,只有在前提条件仍然有效时(例如product.Quantity - requestedQuantity
),代码末尾的写入操作才会发生。
此前提条件在内存中的应用程序端进行评估。但是应用程序此时只能看到数据的快照,当数据库读取发生时:_database.GetProduct();
一旦其他人更新相同的数据,这可能会变得过时。如果您想避免使用SERIALIZABLE
作为事务隔离级别(无论如何都会影响性能),应用程序应在编写时检测前提条件是否仍然有效。或者换一种说法,如果数据在处理时没有改变。
这可以通过使用离线并发模式来完成:optimistic offline lock 或 pessimistic offline lock。许多 ORM 框架默认支持这些功能。
【讨论】:
我真的需要熟悉这些术语,而且我确实学到了一些新东西。谢谢。 很高兴听到!不客气。在“企业应用程序架构的模式”一书中,Martin 甚至解释了实现悲观离线锁和使用 SELECT FOR UPDATE 之间的区别。书有点旧了,但很多东西还是有道理的。以上是关于如果可能有多个服务器(并且每个服务器都可以有多个线程),如何处理竞争条件的主要内容,如果未能解决你的问题,请参考以下文章