我正在为MongoDB`s的“更新”模拟multiple concurrent request。
事情是这样的,我在amount=1000中插入了一个数据mongoDB,每次触发api时,它都会通过amount += 50更新数据并将其保存回数据库。基本上,它是对单个文档的find and update操作。
err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry)
if err != nil {
panic(err)
}
wait := Random(1, 100)
time.Sleep(time.Duration(wait) * time.Millisecond)
//step 3: add current balance and update back to database
entry.Amount = entry.Amount + 50.000
err = globalDB.C("bank").UpdateId(entry.ID, &entry)这里是该项目的源代码。
我正在使用Vegeta模拟请求:
如果我设置了-rate=10(这意味着每秒触发api 10次,所以1000 + 50 * 10 = 1500),那么数据是正确的。
echo "GET http://localhost:8000" | \
vegeta attack -rate=10 -connections=1 -duration=1s | \
tee results.bin | \
vegeta report

但是使用-rate=100(这意味着每秒触发api 100次,所以1000 + 50 * 100 = 6000)会产生非常混乱的结果。
echo "GET http://localhost:8000" | \
vegeta attack -rate=100 -connections=1 -duration=1s | \
tee results.bin | \
vegeta report

简而言之,我想知道的是:我认为MongoDB正在使用optimistic concurrency control,这意味着如果有write conflict,它应该再试一次,这样延迟就会增加,但是数据应该是正确的。
为什么结果看起来在MongoDB中完全不能保证数据的正确性?
我知道你们中的一些人可能会注意到41和42行的睡眠,但是即使我把它注释掉了,当我用-rate=500测试时,结果仍然不正确。
有什么线索吗?
发布于 2020-04-24 08:26:01
通常,您应该将代码的相关片段提取到问题中。要求人们在你的76行程序中找到5条相关线路是不体贴的。
您的测试正在执行并发的查找和修改操作。假设有两个并发过程A和B,每个增量帐户余额为50。起始余额为0。行动的顺序可以是:
A: what is the current balance for account 1234?
B: what is the current balance for account 1234?
DB -> A: balance for account 1234 is 0
DB -> B: balance for account 1234 is 0
A: new balance is 0+50 = 50
A: set balance for account 1234 to 50
DB -> A: ok, new balance for account 1234 is 50
B: new balance is 0+50 = 50
B: set balance for account 1234 to 50
DB -> B: ok, new balance for account 1234 is 50从数据库的角度来看,这里没有“写冲突”。您要求将给定帐户的余额设置为50,两次。
解决这个问题有不同的方法。一种是使用条件更新,使该过程如下所示:
A: what is the current balance for account 1234?
B: what is the current balance for account 1234?
DB -> A: balance for account 1234 is 0
DB -> B: balance for account 1234 is 0
A: new balance is 0+50 = 50
A: if balance in account 1234 is 0, set balance to 50
DB -> A: ok, new balance for account 1234 is 50
B: new balance is 0+50 = 50
B: if balance in account 1234 is 0, set balance to 50
DB -> B: balance is not 0, no update was performed
B: err, let's start over
B: what is the current balance for account 1234?
DB -> B: balance for account 1234 is 50
B: new balance is 50+50 = 100
B: if balance in account 1234 is 50, set balance to 100
DB -> B: ok, new balance for account 1234 is 100正如您所看到的,数据库必须支持条件更新,应用程序必须处理并发更新的可能性并重新尝试操作。
如果余额可以上下浮动,这并不是写借方和信贷系统的一种实际有用的方式(但如果余额只能增加或减少,这实际上是很好的)。在实际系统中,您将使用一个特殊的字段,该字段的目的是标识应用程序检索某些数据时存在的文档的特定版本;更新取决于文档的当前版本是否保持不变,并且每个更新都会增加版本。然后会检测到并发更新,因为版本号是错误的,而不是内容字段。
有一些方法可以在数据库端生成“写冲突”,例如使用MongoDB 4.0+支持的事务。原则上,这是相同的工作方式,但“版本”被称为“事务标识符”,并且存储在不同的位置(而不是在所操作的文档中内联)。但原则是一样的。在这种情况下,数据库会通知您存在写冲突,您仍然需要重新发出操作。
更新:
我认为您还需要区分“乐观的货币控制”作为一个概念,它的实现,以及什么是适用的。例如,https://docs.mongodb.com/manual/faq/concurrency/#how-granular-are-locks-in-mongodb说:
对于大多数读和写操作,WiredTiger使用乐观并发控制。WiredTiger只在全局、数据库和集合级别使用意向锁。当存储引擎检测到两个操作之间的冲突时,其中一个操作将引发写冲突,导致MongoDB透明地重试该操作。
仔细阅读此语句,它适用于存储引擎级别上的写入操作。我设想,当MongoDB执行类似于$set或其他原子写入操作的操作时,这将适用。但这不适用于应用程序级的操作序列,就像您在示例中给出的那样。
如果您尝试使用您最喜欢的关系数据库管理系统的示例代码,我想您会发现它产生的结果与您在MongoDB中看到的结果大致相同,如果您围绕每一个读和写(例如,平衡读和写在不同的事务中)发出一个事务,原因是相同的-- RDBMSes锁数据(或者使用MVCC之类的技术)在事务的生存期内,而不是跨事务。
类似地,如果将相同帐户上的余额读和余额写入到MongoDB中的事务中,您可能会发现,当其他事务同时修改有关帐户时,您可能会收到瞬态错误。
最后,MongoDB为事务实现的API (通过重试)被描述为这里。如果仔细研究它,您会发现它期望应用程序不仅重新发出事务提交命令,而且还会重复整个事务操作。这是因为,通常情况下,如果存在“写冲突”,起始数据已经改变,仅仅再次尝试最终写入是不够的--应用程序中的潜在计算需要重新进行,甚至可能因此导致进程更改的副作用。
https://stackoverflow.com/questions/61391417
复制相似问题