Help us understand the problem. What is going on with this article?

Rails5 で `#increment!` と `#decrement!` が同時実行を意識した処理になった!

More than 3 years have passed since last update.

同時実行になった時におかしくなっちゃう、というその点でずっと使えなかった #increment! が同時実行を意識した処理になったよ!
嬉しい! 素敵!

rails4

rails4
irb(main):001:0> counter = Counter.create
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "counters" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2016-02-22 11:39:38.966295"], ["updated_at", "2016-02-22 11:39:38.966295"]]
   (1.9ms)  commit transaction
=> #<Counter id: 4, value: nil, created_at: "2016-02-22 11:39:38", updated_at: "2016-02-22 11:39:38">

default 設定し忘れて nil になっているけれども valueinteger です。
この value#increment! したいと思います。

rails4
irb(main):004:0> counter.increment!(:value)
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ?  [["value", 1], ["updated_at", "2016-02-22 11:40:48.595257"], ["id", 4]]
   (2.7ms)  commit transaction
=> true

実行された SQL を見ると value に 1 が指定されてるのがわかります。
この 1 は、どこからきたかっていうと、インスタンスがキャッシュしている value の値を元に計算されてるんですね。

つまりこれはどういうことかっていうと、

rails4
irb(main):005:0> counter1 = Counter.find(4)
  Counter Load (0.2ms)  SELECT  "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT 1  [["id", 4]]
=> #<Counter id: 4, value: 1, created_at: "2016-02-22 11:39:38", updated_at: "2016-02-22 11:40:48">
irb(main):006:0> counter2 = Counter.find(4)
  Counter Load (0.1ms)  SELECT  "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT 1  [["id", 4]]
=> #<Counter id: 4, value: 1, created_at: "2016-02-22 11:39:38", updated_at: "2016-02-22 11:40:48">

こんな感じで、同一レコードから生成された別のインスタンスがいる時に、それぞれで同時に #increment! したらどうなるかってことです。

rails4
irb(main):007:0> counter1.increment!(:value)
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ?  [["value", 2], ["updated_at", "2016-02-22 11:43:39.842789"], ["id", 4]]
   (1.8ms)  commit transaction
=> true
irb(main):008:0> counter2.increment!(:value)
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ?  [["value", 2], ["updated_at", "2016-02-22 11:43:45.706247"], ["id", 4]]
   (1.7ms)  commit transaction
=> true

value が 1 の状態から #increment! は 2回呼ばれました。
value が 3 になっていて欲しいのですが、実際には 2 です :worried:

irb(main):011:0> Counter.find(4).value
  Counter Load (0.1ms)  SELECT  "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT 1  [["id", 4]]
=> 2

この同時実行に #decrement! が混ざってくると、もう何が何やらです。

rails4
irb(main):024:0> counter1.increment!(:value, 10)
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ?  [["value", 12], ["updated_at", "2016-02-22 11:50:37.442805"], ["id", 4]]
   (1.8ms)  commit transaction
=> true
irb(main):025:0> counter2.increment!(:value, 20)
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ?  [["value", 22], ["updated_at", "2016-02-22 11:50:52.249161"], ["id", 4]]
   (1.7ms)  commit transaction
=> true
irb(main):026:0> counter1.decrement!(:value, 10)
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ?  [["value", 2], ["updated_at", "2016-02-22 11:51:04.072007"], ["id", 4]]
   (2.7ms)  commit transaction
=> true
irb(main):027:0> Counter.find(4).value
  Counter Load (0.1ms)  SELECT  "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT 1  [["id", 4]]
=> 2

20 はどこに消えた :worried:

rails5

さあ、同じように Counter インスタンスを作ります。

rails5
irb(main):001:0> counter = Counter.create
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "counters" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", 2016-02-22 11:47:23 UTC], ["updated_at", 2016-02-22 11:47:23 UTC]]
   (2.9ms)  commit transaction
=> #<Counter id: 5, value: nil, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">

おもむろに #increment! しましょう。

rails5
irb(main):002:0> counter.increment!(:value)
  SQL (3.2ms)  UPDATE "counters" SET "value" = COALESCE("value", 0) + 1 WHERE "counters"."id" = ?  [["id", 5]]
=> #<Counter id: 5, value: 1, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">

発行された SQL を見ると UPDATE 文が変わったのがわかるかと思います。
"value" = COALESCE("value", 0) + 1 となっていますね(嬉

先ほどと同じように、同一のレコードから生成されたインスタンスを複数作って操作してみます。

rails5
irb(main):003:0> counter1 = Counter.find(5)
  Counter Load (0.2ms)  SELECT  "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
=> #<Counter id: 5, value: 1, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):004:0> counter2 = Counter.find(5)
  Counter Load (0.2ms)  SELECT  "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
=> #<Counter id: 5, value: 1, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">

irb(main):005:0> counter1.increment!(:value)
  SQL (2.1ms)  UPDATE "counters" SET "value" = COALESCE("value", 0) + 1 WHERE "counters"."id" = ?  [["id", 5]]
=> #<Counter id: 5, value: 2, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):006:0> counter2.increment!(:value)
  SQL (2.1ms)  UPDATE "counters" SET "value" = COALESCE("value", 0) + 1 WHERE "counters"."id" = ?  [["id", 5]]
=> #<Counter id: 5, value: 2, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):007:0> Counter.find(5).value
  Counter Load (0.2ms)  SELECT  "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
=> 3

今度はちゃんと value が 3 になっています :smile:

#decrement! だって大丈夫!

rails5
irb(main):008:0> counter1.increment!(:value, 10)
  SQL (2.2ms)  UPDATE "counters" SET "value" = COALESCE("value", 0) + 10 WHERE "counters"."id" = ?  [["id", 5]]
=> #<Counter id: 5, value: 12, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):009:0> counter2.increment!(:value, 20)
  SQL (2.2ms)  UPDATE "counters" SET "value" = COALESCE("value", 0) + 20 WHERE "counters"."id" = ?  [["id", 5]]
=> #<Counter id: 5, value: 22, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):010:0> counter1.decrement!(:value, 10)
  SQL (2.2ms)  UPDATE "counters" SET "value" = COALESCE("value", 0) - 10 WHERE "counters"."id" = ?  [["id", 5]]
=> #<Counter id: 5, value: 2, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):011:0> Counter.find(5).value
  Counter Load (0.2ms)  SELECT  "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
=> 23

ただし #increment!#decrement! した後は、「実際に DB に保存されている値」と「インスタンスのキャッシュ」にズレが出ている可能性があるというのは、意識しておいた方が良さそうです。

rails5
irb(main):012:0> counter1.value
=> 2
irb(main):013:0> counter2.value
=> 22
kano-e
feedforce
『「働く」を豊かにする。』というミッションを掲げ、企業向けネットサービスを開発・提供しています。
https://www.feedforce.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした