search
LoginSignup
26

More than 5 years have passed since last update.

Organization

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

同時実行になった時におかしくなっちゃう、というその点でずっと使えなかった #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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
26