使用Rails find_or_create处理PostgreSQL并发

时间:2014-05-22 09:24:55

标签: ruby-on-rails postgresql concurrency transactions

由于某些原因,如果不同的用户在同一时间运行该代码,则此代码可以创建重复的游戏:

game = Game.find_or_create_by(
    status: Game::STATUS[:waiting],
    category_id: params[:category_id],
    private: 0
) do |g|
  is_new = true
  g.user = current_user
end

我无法清楚地知道是什么问题,但可能是关于使用不同数据库连接的不同Unicorn进程,因此事务可以并行运行。

如果是这样,我需要正确的方法来避免它,也许我应该使用Rails事务或Postgres锁,但我真的需要一个使用的例子。

谢谢。

1 个答案:

答案 0 :(得分:1)

可以以高并发级别发生。

根据rails docs,这些查询将运行:

SELECT * FROM games WHERE status = 'waiting' AND ... LIMIT 1;
INSERT INTO games (status, ...) VALUES ('waiting', ...);

当第一个避难所返回时,第二个只运行。

如果两个(或更多)连接在几微秒内启动第一个查询,则多个进程可能会创建多个条目。为防止这种情况发生,您可以在该表格上使用ACCESS EXCLUSIVE lock,或使用自定义advisory lock

您也可以使用一些唯一索引,以防止将多个条目插入到您的数据库中,但如果它自己使用,则会在这种情况下导致SQL异常。

修改

可以通过LOCK command获取

ACCESS EXCLUSIVE锁。

可以使用pg_advisory_lock(id) function获取咨询锁定。

两者都要求您运行任意SQL命令。


另一种方法是使用自定义查询:

  1. 仅在不存在时插入(&返回所有字段)
  2. 仅在未插入时选择
  3. 东西,比如:

    INSERT INTO games (status, category_id, private)
         SELECT 'waiting', 2, 0
          WHERE NOT EXISTS (
                 SELECT 1
                   FROM games
                  WHERE status = 'waiting'
                    AND category_id = 2
                    AND private = 0
           )
      RETURNING *;
    
    -- only select, when this not inserted anything
    
    SELECT *
      FROM games
     WHERE status = 'waiting'
       AND category_id = 2
       AND private = 0