MongoDB的upsert在并行程序中应该注意的问题

题图

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倍。这多处来的速度,就是唯一索引导致的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,893评论 2 89
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 有一天 我没钱了 没工作了 没爱人了 没家人了 我要怎么活...
    醉酒少年阅读 310评论 0 1
  • 我总是很难为情自己不懂音乐,不记得人名,不知道曲名。唯一印象深刻的是贝多芬,因为课本里有月光曲。还有就是他耳朵聋了...
    青华浅语阅读 658评论 2 1
  • 今天运动会开幕式的第一天,在前一晚上,我就已经激动的不行了。 今天运动会正式开始了,我7:50分到了学校,不一会儿...
    左岸的微笑_a648阅读 321评论 0 0