[教程] Ruby on Rails 的 first_or_initialize 会造成 race condition,解决办法是用 upsert

把 first_or_initialize 改成 upsert 就行

[教程] Ruby on Rails 的 first_or_initialize 会造成 race condition,解决办法是用 upsert

今天(2020-3-4)没有心情写长文配图片,简单写几句就行

这篇文章讲什么?

Ruby on Rails 里 first_or_initialize "可能"会出错。
之所以说"可能":这个问题只会在多个 Rails 实例下造成问题,
如果你只是本地开发,只有1个 Rails server。这个问题是测不出来的

对谁有用?

Ruby on Rails 程序员

有什么用?

修 bug

问题描述

程序员使用 first_or_initialize 的本质是:
要么插入,要么更新,我不管,反正你给我存下来。

如果先

xxx.first_or_initialize

然后

xxx.save

因为它是先 select check 是不是存在,然后去 insert。
所以有 race condition 问题。

如果你的数据库设置了唯一索引(比如这里我们用的是 PostgreSQL)

那么会报错。

ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_apply_evaluations_on_unique_columns"
DETAIL:  Key (application_id, stage_key, user_id)=(1259, interview, 57) already exists.

具体报错信息别管,大概扫一眼就行,不需要你认真读。
总之意思是数据重复了。索引不能重复。

注:如果你的数据库没有设置唯一索引,当然也不会报这个错误。
如果你的 Model 不验证任何唯一性,
那么数据库会多出两条一样的数据。

问题根源

我们线上用的是 AWS ECS,可以类比成 Docker Swarm 和 Kubernetes。
跑了3份容器。
用户点击2次按钮时,发了2个请求,这两个请求到达了3个容器里面的2个。
同时执行,同时 first_or_initialize。
都 new 了一个 Record。
然后 save 的时候,其中一个成功了,另一个就报错。

解决办法

  1. 不理。因为数据的确保存了,没有造成其他任何问题。
  2. 使用 Rails 6.0.0 里面的 upsert。

我们是怎么解决的

因为这个问题并没有阻碍用户正常使用功能。我们决定下次处理
下次的处理方式是:

  1. 把 Rails 从 6.0.0.rc1 更新到 6.0.0
  1. 改下代码,把 first_or_initialize 改成 upsert。

全文完

感谢阅读