作者:爱恨情仇4131_120 | 来源:互联网 | 2023-09-25 09:22
题图Oct.12th,2018最近使用python的多进程编程来解决一个MongoDB的数据库。这个程序中,每一个进程都要对数据库中的某些数据进行升级。假如目标数据不存在的话,则需
题图
Oct. 12th, 2018
最近使用python的多进程编程来解决一个MongoDB的数据库。这个程序中,每一个进程都要对数据库中的某些数据进行升级。假如目标数据不存在的话,则需要进行upsert。程序运行之后,速度的确比单进程快少量(在小数据集的情况下,大约快了三四倍),但是最终结果却与单进程产生的结果不符合。研究一番之后,把问题锁定在了多进程同时升级数据库时产生的upsert问题。这里探讨一个简化模型来阐明此问题并且论述处理方法。
简化模型
为了简化探讨,这里探讨一个投票程序。某城市人民选举该市市长。每个市民选择一个人为他/她投票。因为该选举为广泛选举,在选举之前并没有候选人集合,而是每个市民可以把票投给任何一个人。为简化问题,假设城市里没有人重名,因而每个人的名字可以作为其唯一代号使用。实际情况中,可以使用其余唯一表示,比方身份证号等等。
这里的数据集非常简单:
Collection: db.election{'name': 'Alice', 'votes': 10}{'name': 'Bob', 'votes': 21}...
作为一个计票的进程,主要任务就是拿过一张选票,查看其name属性,在数据库中给名字为name的文档的票数加1。注意,这里name不肯定已经存在于数据库中。假如此名字不存在,则应新建一条文档。因而,使用update_one时upsert
属性应设为True
。
# name variable stores the candidate's name.db.election.update_one({'name': name}, \ {'$inc': {'votes': 1}}, upsert=True)
在只有一个进程运行的情况下,这段代码尽管速度并不快,但会给出正确的计票结果。假如我们使用多进程,创立几个worker,分别收集选票,给指定的被选人计票,会怎样样呢?
多进程下的写入矛盾
当简单地把上面的update_one交给几个进程来解决的时候,我们会发现运行结果出了这样的问题:每个被选人的票数似乎少了很多,而被选人的数量添加了。仔细检查会发现,其实是同一个候选人被创立了多个文档。为什么会导致这样的写入错误呢?不难想象到,这是多个进程同时试图升级一个文档的时候导致的。
需要注意的是,MongoDB本身是有文档级的写入锁的。也就是说,当一个进程开始修改一个文档时,该文档被锁定,其余文档不可以再对其进行写入甚至读取。这个写入锁的存在本身就是为了防止不同程序升级文档时产生的写入冲突。然而,update
其实分为两步。首先是搜索文档位置,而后是文档升级。当两个程序同时试图升级一个不存在的文档的时候,假设程序A先发现文档不存在,而后程序B发现文档不存在。此时A还没来得及对文档进行写入,因而文档锁并没有挂起。或者者说,因为文档不存在,探讨文档锁也就失去了意义。这个时候,两个进程就会分别创立文档并给其votes加1。于是就出现了不必要的重复。
如何处理?
处理方法其实很简单:unique index。 上文提到,name属性是唯一的。假如我们给它加一个唯一索引,不即可以从根本上避免一个人有多个不同的文档了吗?这个时候,即便两个进程经过搜索都得到了某个文档不存在的结果,假设A先一步创立了该文档,那么当B创立文档时,因为含有相同name的文档已经被A进程抢先创立,MongoDB就会拒绝B进程创立。pymongo
对此类错误应该是有应对机制的,这是B进程会稍等片刻,重新尝试升级文档。这个时候,A进程已经完成计票并且释放了写入锁,文档被成功创立,而进程B再尝试时,也会检索到这个被新创立的文档,直接在上面把票数加1,而不是创立新文档。这样一个小的时间差,就处理了写入矛盾。
同时,我们还得到了额外的奖励:当name
上创立了unique index之后,找到特定候选人的速度就会快很多。这个优势在计票初期,候选人数量不多时并没有显示,但当后期候选人数变多时,一方面再有新的候选人被加入的概率会变得很小(该被加的差不多都被加进来了),因而修改索引的几率越来越少;另一方面,在候选人基数变得很大的时候,相比于没有索引的情况,有唯一索引的情况下程序的速度优势会越发显著。这两个方面综合在一起,结果就是,增加唯一索引之后程序在后期速度优势会越来越显著。在我自己的程序中,运行初期多线程比单线程只快了三四倍,但在数据量较大时,多线程(加上唯一索引)会比单线程快10到20倍。这多处来的速度,就是唯一索引导致的。