Posted at

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