Mongoid 4(GitHub master)创建具有重复ID的文档

时间:2014-01-10 21:42:39

标签: ruby-on-rails ruby mongoid sidekiq

我正在使用Sidekiq进行高流量测试,使用Mongoid作为Rails 4应用程序中的驱动程序创建基于MongoDB的对象。我看到的问题是,当一个PlayByPlay文档应该具有唯一的game_id时,我会看到多个PlayByPlay对象是使用完全相同的game_id创建的。我也对MongoDB实施了唯一约束,这仍然存在。这是我的文档,它是嵌入式文档,以及我如何创建文档的一瞥。问题是这一切都发生在使用Sidekiq的线程环境中,我不确定是否有办法解决它。我的写入问题设置为1中的mongoid.yml,看起来safe选项在主设备中被移除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名工人,这个问题就会消失,这显然是在我从未做过的现实世界中。

6 个答案:

答案 0 :(得分:3)

您已经拥有game_id的索引,为什么不让它独一无二?这样db就不允许重复输入,即使mongoid无法正确进行验证( @vidaica 的答案描述了mongoid如何无法验证唯一性)。

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

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

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

答案 1 :(得分: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. 的文档
  4. t1保存文件

  5. t2保存文件

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

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

答案 2 :(得分:1)

无论你做什么,你都希望在数据库级别上解决这个问题,因为你几乎肯定会做出最糟糕的工作来实现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

答案 3 :(得分:0)

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

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

您的game_id的生成方式/位置在哪里?

答案 4 :(得分:0)

一种天真的方法是将#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

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

答案 5 :(得分:0)

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

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