You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flask+MongoDB REST API并发PUT请求下如何避免同书名子文档重复?

问题

使用Flask+MongoDB(pymongo)开发REST API时,并发PUT请求为同一用户的同书名书籍设置不同字段,出现重复子文档的竞态条件问题。

集合初始状态

[
  {
    "name": "Mike",
    "books": [
      {
        "title": "Book 1",
        "minutes-spent-reading": 100,
        "date-bought": "2024-09-23"
      }
    ]
  }
]

并发PUT请求

  1. 请求1:
PUT /name/Mike/books/Book 2
Content-Type: application/json
{"minutes-spent-reading": 40}
  1. 请求2:
PUT /name/Mike/books/Book 2
Content-Type: application/json
{"date-bought": "2024-09-26"}

预期结果

集合中仅存在一个title为"Book 2"的子文档,包含两个字段。

实际结果

生成两个同书名的子文档:

[
  {
    "name": "Mike",
    "books": [
      {
        "title": "Book 1",
        "minutes-spent-reading": 100,
        "date-bought": "2024-09-23"
      },
      {
        "title": "Book 2",
        "minutes-spent-reading": 40
      },
      {
        "title": "Book 2",
        "date-bought": "2024-09-26"
      }
    ]
  }
]

复现代码

import threading
import pymongo
import time
import json

def put_book_2_minutes_spend_reading(value):
    # Find book in the array of books
    book = db.experiment.find_one({
        'name': 'Mike',
        'books.title': 'Book 2'
    })
    # Simulate processing delay
    time.sleep(1)
    if book == None:
        db.experiment.find_one_and_update({
            'name': 'Mike',
        }, {
            '$push': {
                'books': {
                    'title': 'Book 2',
                    'minutes-spent-reading': value
                }
            }
        })
    else:
        db.experiment.find_one_and_update({
            'name': 'Mike'
        }, {
            '$set': {
                'books.$[book].minutes-spent-reading': value
            }
        }, array_filters = [{'book.title': 'Book 2'}])

def put_book_2_date_bought(value):
    book = db.experiment.find_one({
        'name': 'Mike',
        'books.title': 'Book 2'
    })
    time.sleep(1)
    if book == None:
        db.experiment.find_one_and_update({
            'name': 'Mike',
        }, {
            '$push': {
                'books': {
                    'title': 'Book 2',
                    'date-bought': value
                }
            }
        })
    else:
        db.experiment.find_one_and_update({
            'name': 'Mike'
        }, {
            '$set': {
                'books.$[book].date-bought': value
            }
        }, array_filters = [{'book.title': 'Book 2'}])

db = pymongo.MongoClient('localhost', 27017)['experiment']
db.experiment.delete_many({})
# Sample item
db.experiment.insert_one({
    'name': 'Mike',
    'books': [
        {
            'title': 'Book 1',
            'minutes-spent-reading': 100,
            'date-bought': '2024-09-23'
        }
    ]
})

t1 = threading.Thread(target=put_book_2_minutes_spend_reading, args=[40])
t2 = threading.Thread(target=put_book_2_date_bought, args=['2024-01-20'])

t1.start()
t2.start()

t1.join()
t2.join()

print(json.dumps(list(db.experiment.find({}, {"_id": 0})), indent=2))

解决方案

问题根源是先查询后更新的非原子操作:两个并发请求都先查到目标子文档不存在,随后都执行$push插入操作。解决核心是将判断和更新合并为原子操作,利用MongoDB单写操作的原子性避免竞态。

推荐实现方式

修改更新逻辑,先尝试原子更新已存在的子文档;若更新未命中(说明子文档不存在),再执行原子性插入(仅当子文档确实不存在时才插入):

def update_book_minutes_spent(username, book_title, value):
    # 原子更新已存在的子文档
    update_result = db.experiment.update_one(
        {"name": username},
        {"$set": {"books.$[book].minutes-spent-reading": value}},
        array_filters=[{"book.title": book_title}]
    )
    # 若未找到可更新的子文档,原子插入新文档(仅当该title不存在时)
    if update_result.modified_count == 0:
        db.experiment.find_one_and_update(
            {"name": username, "books.title": {"$ne": book_title}},
            {"$push": {"books": {"title": book_title, "minutes-spent-reading": value}}}
        )

def update_book_date_bought(username, book_title, value):
    update_result = db.experiment.update_one(
        {"name": username},
        {"$set": {"books.$[book].date-bought": value}},
        array_filters=[{"book.title": book_title}]
    )
    if update_result.modified_count == 0:
        db.experiment.find_one_and_update(
            {"name": username, "books.title": {"$ne": book_title}},
            {"$push": {"books": {"title": book_title, "date-bought": value}}}
        )

原理说明

MongoDB的单个写操作具备原子性,上述逻辑中:

  1. 第一步更新操作会原子性地匹配并修改已存在的子文档
  2. 第二步插入操作带有条件(books.title != 目标书名),只有当子文档确实不存在时才执行插入
  3. 并发场景下,第一个请求完成插入后,第二个请求的插入条件会不满足,转而执行更新操作,最终只会存在一个目标子文档

内容的提问来源于stack exchange,提问作者Rodrigo Morales

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 21:15:53