Hatena::ブログ(Diary)

アルミ缶の上にアルフォート このページをアンテナに追加 RSSフィード Twitter

2015-01-11

とあるMySQLとチョコレートの話

何回もログを仕込んでようやく原因がわかったバグを潰したメモです。


1ユーザーにつき1個だけチョコレートを作りたいとします。

※今回はDBのunique制約でどうこうするとかいうレベルではなく、チョコレートが2回作られる時点でアウトとします。

以下の挙動をもつAPIをつくります。

リクエストユーザー挙動
1まだチョコレートをもってないユーザーチョコレートを作ってあげて、作ったチョコレートを返す
2チョコレートをもっているユーザーチョコレートを返す

というわけで、以下のかんじで作ってみました。

function get_or_create_choco(user_id) {
  var choco = select_choco(user_id); //DB(master)からチョコレートをとる
  if (choco) {
    return choco;
  }
  else {
    choco = create_choco(user_id); //チョコレートを作ってDBにinsert
    return choco;
  }
}

これはだめぽよです。

リクエストが並列で2回きた場合、create_chocoが2回呼ばれてしまうのです。

というわけで、例えばmemcachedでロックを貼ってみました。


function get_or_create_choco(user_id) {
  var choco = select_choco(user_id); //DB(master)からチョコレートをとる
  if (choco) {
    return choco;
  }
  else {
    var lock = create_choco_lock(user_id) //memcachedで特定のキーでaddしてみる
    if (lock) { //ロック取得成功時
      choco = create_choco(user_id); //チョコレートを作ってDBにinsert
      return choco;
    }
    else { //ロック取得失敗時
   sleep 0.2;
      choco = select_choco(user_id); //DB(master)からチョコレートをとる
      return choco;
    }
  }
}


こうすれば、短期間にきた2回目のリクエストはロックを取得できないので、

0.2秒くらい休んだあとにDBを見に行けば、既に1回目のリクエストでチョコレートは作られているはずなので、

無事に作られたチョコレートを返せるはずです。


もう一工夫。これはかなり頻繁に叩かれる処理なので、ちょっとmemcachedを効かせてみます。


function get_or_create_choco(user_id) {
  var choco = select_choco(user_id); // memdからチョコを取る。なかったらDB(master)からチョコを取る。DBにあったらmemdにセットしてあげる。
  if (choco) {
    return choco;
  }
  else {
    var lock = create_choco_lock(user_id) //memcachedで特定のキーでaddしてみる
    if (lock) { //ロック取得成功時
      choco = create_choco(user_id); //チョコレートを作ってDBにinsert
      return choco;
    }
    else { //ロック取得失敗時
   sleep 0.2;
      choco = select_choco(user_id); // memdからチョコを取る。なかったらDB(master)からチョコを取る。DBにあったらmemdにセットしてあげる。
      return choco;
    }
  }
}

これで完成。

しかし低確率で、なぜかチョコレートを返せないときがありました。

もろもろログを増やしたところ、どうもロック取得失敗時に、select_chocoしてもチョコが取れないようなのです。つまり、2回目のリクエストにおいて、memdにもDBにもチョコがないと言われている。


色々原因を考えました。

・sleepが足りない?

 ・休む時間を伸ばしてあげてもダメ

・1回目のリクエストにおいて、memdやmysqlでのエラーはおきてる?

 ・おきてない

・チョコのmemdへのsetが失敗している?

・memdは複数台あるので、同じkeyなのに別のmemdを見に行ってる?

 ・memdになかったらDBにfallbackするし関係なさそう

・memd evictionしてる?

 ・してない

・memdに「チョコなかったよー」ってネガティブキャッシュしてる?

 ・してない


答えをいうと、REPEATABLE READとAutoCommit=0によるものでした。

AutoCommit=0でmasterに接続しており、コネクションを使いまわしていたので、2回のselect_chocoでトランザクションが継続していました ><

問題となるケースは、一番頭でselect_chocoをしていて空振っています。で、同じconnectionを使いまわしているので、それ以降は他のプロセスがいくらchocoを作っていても、いくらsleepしても「このユーザーはチョコがない」ことが確定してしまっています。シュレーディンガーの猫ですね。

そもそもcreate_chocoでmemdにセットすべきなのにしていないので、(短期間における)2回目のリクエストでは必ずDBを覗きにきてしまい、チョコがないことが確定しているのであぼーんという話。

基本はmaster DBからはちゃんとcommitして抜けているのですが、select_chocoのときだけ単なるselectじゃーんということで、commitしておりませんでした ><

あとはcommitとかコネクション使い回しとかがライブラリでいいかんじに隠蔽されてたのであんまり強く意識してなかったのも敗因です。。


最終的には、

(1)select_chocoではスコープから抜けるときにちゃんとcommitしてトランザクションを完結させる

(2)create_chocoでちゃんとチョコをmemdにもセットする

かんじで修正しました。


皆さんもシュレーディンガーの猫には気をつけましょう。単なるselectもcommitしないと罠になるかもよ。というお話なのでした。

mosa_sirumosa_siru 2015/01/12 00:32 デッドロック絶対にしちゃいけないとこだったからmysql lockを避けたのだけど、今おもえば使ったほうが良かったかもしれない

key_ambkey_amb 2015/01/12 01:10 それか最初 DB select するときは auto_commit=1 にしとくかですかねー。参照するトランザクションはそうしておいた方がいいかと思います。

それでも insert して commit するまでのわずかな間に別のセッションが割り込んでくる可能性はなくはないですが、memd によるロック取得が効いているならそれは起こらなさそうです。

> デッドロック絶対にしちゃいけないとこだったからmysql lockを避けたのだけど、今おもえば使ったほうが良かったかもしれない

これは SELECT 〜 FOR UPDATE でロックを取るということですかね? REPEATABLE READ の場合は NEXT KEY LOCK で前後のキーも含めてロック取られてしまうので、頻繁に叩かれるのであれば十分に注意したほうがいいと思います。

mosa_sirumosa_siru 2015/01/12 01:55 >それか最初 DB select するときは auto_commit=1 にしとくかですかねー。参照するトランザクションはそうしておいた方がいいかと思います。
たしかに!!

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/mosa_siru/20150111/1420984414
おとなり日記