【问题标题】:Mongoid 4 (GitHub master) creating documents with duplicate IDsMongoid 4 (GitHub master) 创建具有重复 ID 的文档
【发布时间】:2014-01-20 17:08:41
【问题描述】:

我正在使用 Sidekiq 运行高流量测试,该测试使用 Mongoid 作为我在 Rails 4 应用程序中的驱动程序创建基于 MongoDB 的对象。我看到的问题是,当PlayByPlay 文档应该具有唯一的game_id 时,我看到多个 PlayByPlay 对象使用相同的确切 game_id 创建。我也对 MongoDB 实施了唯一约束,这仍在发生。这是我的文档,它是嵌入式文档,以及我如何创建文档的一瞥。问题是这一切都发生在使用 Sidekiq 的线程环境中,我不确定是否有办法解决它。我的写入问题在mongoid.yml 中设置为1,看起来safe 选项在master 中被删除,persist_in_safe_mode 也是如此。下面的代码 - 关于如何正确工作的任何建议将不胜感激。这不是一个副本集,它是一个单一的 MongoDB 服务器,此时执行所有读/写请求。

module MLB
    class Play
        include Mongoid::Document
        include Mongoid::Timestamps

        embedded_in :play_by_play

        field :batter#, type: Hash
        field :next_batter#, type: Hash
        field :pitchers#, type: Array
        field :pitches#, type: Array
        field :fielders#, type: Array
        field :narrative, type: String
        field :seq_id, type: Integer
        field :inning, type: Integer
        field :outs
        field :no_play
        field :home_team_score
        field :away_team_score
    end
    class PlayByPlay 
        include Mongoid::Document
        include Mongoid::Timestamps

        embeds_many :plays, cascade_callbacks: true
        accepts_nested_attributes_for :plays

        field   :sport
        field :datetime, type: DateTime
        field :gamedate, type: DateTime
        field :game_id
        field :home_team_id
        field :away_team_id
        field :home_team_score
        field :away_team_score
        field :season_year
        field :season_type
        field :location
        field :status
        field :home_team_abbr
        field :away_team_abbr
        field :hp_umpire
        field :fb_umpire
        field :sb_umpire
        field :tb_umpire

        index({game_id: 1})
        index({away_team_id: 1})
        index({home_team_id: 1})
        index({season_type: 1})
        index({season_year: 1})

        index({"plays.seq_id" => 1}, {unique: true, drop_dups: true})
        #validates 'play.seq_id', uniqueness: true
        validates :game_id, presence: true, uniqueness: true
        validates :home_team_id, presence: true
        validates :away_team_id, presence: true
        validates :gamedate, presence: true
        validates :datetime, presence: true
        validates :season_type, presence: true
        validates :season_year, presence: true

        def self.parse!(entry)
            @document = Nokogiri::XML(entry.data)
            xslt = Nokogiri::XSLT(File.read("#{$XSLT_PATH}/mlb_pbp.xslt"))
            transform = xslt.apply_to(@document)
            json_document = JSON.parse(transform)

            obj = find_or_create_by(game_id: json_document['game_id'])
            obj.sport                   = json_document['sport']
            obj.home_team_id        = json_document['home_team_id']
            obj.away_team_id        = json_document['away_team_id']
            obj.home_team_score = json_document['home_team_score']
            obj.away_team_score = json_document['away_team_score']
            obj.season_type         = json_document['season_type']
            obj.season_year         = json_document['season_year']
            obj.location                = json_document['location']
          obj.datetime              =   DateTime.strptime(json_document['datetime'], "%m/%d/%y %H:%M:%S")
            obj.gamedate                = DateTime.strptime(json_document['game_date'], "%m/%d/%Y %H:%M:%S %p")
            obj.status                  = json_document['status']
            obj.home_team_abbr  = json_document['home_team_abbr']
            obj.away_team_abbr  = json_document['away_team_abbr']
            obj.hp_umpire           = json_document['hp_umpire']
            obj.fb_umpire           = json_document['fb_umpire']
            obj.sb_umpire           = json_document['sb_umpire']
            obj.tb_umpire           = json_document['tb_umpire']
            p=obj.plays.build(seq_id: json_document['seq_id'])
            p.batter            =   json_document['batter']
            p.next_batter = json_document['next_batter'] if json_document['next_batter'].present? && json_document['next_batter'].keys.count >= 1
            p.pitchers      = json_document['pitchers'] if json_document['pitchers'].present? && json_document['pitchers'].count >= 1
            p.pitches       =   json_document['pitches'] if json_document['pitches'].present? && json_document['pitches'].count >= 1
            p.fielders      = json_document['fielders'] if json_document['fielders'].present? && json_document['fielders'].count >= 1
            p.narrative     =   json_document['narrative']
            p.seq_id            = json_document['seq_id']
            p.inning            = json_document['inning']
            p.outs              = json_document['outs']
            p.no_play       =   json_document['no_play']
            p.home_team_score = json_document['home_team_score']
            p.away_team_score = json_document['away_team_score']

            obj.save
        end

    end
end

** 注意 **

如果我将 sidekiq 限制为 1 个工人,这个问题就会消失,这显然在现实世界中我永远不会这样做。

【问题讨论】:

    标签: ruby-on-rails ruby mongoid sidekiq


    【解决方案1】:

    您已经在game_id 上建立了索引,为什么不让它唯一呢?这样,即使 mongoid 无法正确进行验证,数据库也不会允许重复条目(@vidaica 的答案描述了 mongoid 如何无法验证唯一性)。

    尝试添加唯一索引
    index({"game_id" => 1}, {unique: true})
    然后
    rake db:mongoid:create_indexes

    在 mongo 中创建它们(请确保它是从 mongo shell 创建的)。

    之后,mongodb 不应保留任何具有重复 game_id 的记录,您必须在 ruby​​ 层上处理您将从 mongodb 收到的插入错误。

    【讨论】:

      【解决方案2】:

      这是因为许多线程插入具有相同 game_id 的对象。让我解释一下。

      例如,您有两个 sidekiq 线程 t1 和 t2。它们并行运行。假设您有一个带有game_id 1 的文档,并且尚未插入到数据库中。

      1. t1 进入parse 方法,在game_id 1 的数据库中没有看到文档,它创建了一个带有game_id 1 的文档并继续填充其他数据,但它没有保存该文档。

      2. t2 进入parse 方法,它在game_id 1 的数据库中没有看到文档,因为此时t1 还没有保存文档。 t2 创建一个具有相同game_id 1 的文档。

      3. t1 保存文档

      4. t2 保存文档

      结果:您有两个具有相同game_id 1 的文档。

      为了防止这种情况,您可以使用 Mutex 来序列化解析代码的访问。要了解如何使用互斥锁,请阅读以下内容:http://www.ruby-doc.org/core-2.0.0/Mutex.html

      【讨论】:

      • 另一种解决方案是在给定时间后执行任务。 MyWorker.perform_in(5.seconds, 1, 2, 3) 不过这可能是不可接受的。
      • Mutex 几乎使工作人员的水平可扩展性成为问题,理想情况下,您需要在集群中锁定的东西
      • 如果 OP 在许多不同的计算机上运行 sidekiq 线程,@bbozo Mutex 在这种情况下将不起作用。但如果 OP 只在一台计算机上运行它们,它就可以工作。如果 OP 在多台计算机上运行 sidekiq 线程,他可能需要其他解决方案。
      • 希望这就是我所说的 :) Mutex 只要在一台计算机上运行一个进程就可以工作,afaik 如果你在同一台计算机上运行另一个 sidekiq 进程它会中断,如果你运行 sidekiq 进程它肯定会中断在不同的电脑上。考虑到 OP 对高负载下的性能的担忧,将水平可伸缩性排除在设计之外可能不是一个好的选择。解决方案在 mongo 级别的某个地方
      【解决方案3】:

      无论你做什么,你都希望在数据库级别解决这个问题,因为你几乎肯定会在实现唯一约束方面做得比 mongo 人所做的最差。

      假设您有一天想要分片或考虑使用 mongo,因为它具有水平可扩展性功能(您正在进行大量测试,所以我认为这是您不想通过设计排除的事情),可能没有可靠的方法来做到这一点(见Ramifications of working with a mongodb clustersharding concepts):

      假设我们正在对电子邮件进行分片,并希望在用户名上有一个唯一索引。这无法通过集群强制执行。

      但是,如果您在 game_id 上进行分片,或者您根本不考虑分片,那么在 game_id 上设置唯一索引应该可以防止重复记录(请参阅 @xlembouras 答案)。

      但是,当由于竞争条件而违反此索引时,该答案可能无法防止异常,因此请务必抢救该异常并执行更新而不是在救援块中创建(可能通过玩@new_record (click 'Show source'),将尝试找时间给你准确的代码)。

      更新,简短的快速回答

      begin
        a = Album.new(name: 'foo', game_id: 3)
        a.save
      rescue
        a.id = id_of_the_object_with_same_id_already_in_db
        a.instance_variable_set('@new_record', false)
        a.save
      end
      

      【讨论】:

        【解决方案4】:

        @vidaica 的回答很有帮助。如果您从内存或数据库中获取和增加 ID,它可能会解决您的问题。

        但是,您的game_id 不是在parse 中生成的,而是通过entry JSON 对象传递给parse

        您的game_id 是如何/在哪里生成的?

        【讨论】:

        • 在这种情况下,我想 game_id 的来源并不重要。多个文档/条目具有相同的 game_id 但不同的播放可能是合理的情况。如果处理具有重复 game_id 的文档,他仍然会调用 find_or_create_by,因此如果没有 @vidaica 描述的并发性,他将永远不会获得具有相同 game_id 的两条记录。
        • 但是,如果两个文档也不应该具有相同的 game_id,那么另一个问题是异步调用 worker 函数时潜在的并发问题。但是存在 find_or_create_by() 并且仍然存在重复 game_ids 的问题这一事实清楚地表明,至少存在来自 vidaica 答案的并发问题。可能这不是唯一的问题,但它是第一个要解决的问题。
        【解决方案5】:

        也许你应该做一个 upsert 而不是插入:

        obj = new(game_id: json_document['game_id'])
        obj.upsert
        

        【讨论】:

          【解决方案6】:

          一种天真的做法是将#parse的最后一行改为:

          obj.save if where(game_id: obj.game_id).count == 0
          

          或者如果你以某种方式处理它:

          if where(game_id: obj.game_id).count == 0
            # handle it here
          end
          

          但是请注意,这仍然存在重复的可能性。

          【讨论】:

          • 此解决方案无法消除插入具有相同game_id的文档。因为 obj.save 需要时间,当它完成保存时,其他线程可能会到达检查代码,但仍然没有看到具有相同 game_id 的现有文档,然后插入具有相同 game_id 的其他文档
          • 它也不能解决两个线程同时执行计数的竞争情况
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-06-23
          • 2013-01-26
          • 1970-01-01
          • 1970-01-01
          • 2022-10-16
          相关资源
          最近更新 更多