こんな障害が出た、って話で笑い飛ばしてくだせー。
正直、ORMapperでN+1問題とかはよく聞くけど、この挙動は想定外で笑ってしまったので次の犠牲者が出ないことを祈る。
なお半年前に起こった障害なので、ざっくりとした原因は覚えていますが、対応内容とか詳細は完全に忘れてしまっています。
間違えていたら申し訳ない。
環境
- java
- Mybatis(ORMapper)
事象として何が起こったのか
insert処理もしていない、update処理もしていない。
Java側でデータをセットするような処理もしていない。
なのになぜか、処理が終わるとレコードが重複して増えている。
根本原因
- Mybatisでデータを取得するときに、ResultMapで主キー項目にid属性をちゃんと設定できていなかった
- その項目をLazyLoadで取得していたため
lazyLoadとは
必要になるまでオブジェクトを生成しない仕組み。
一気に生成すると、処理に時間がかかってしまうから、必要になったときにデータを取得する。
Twitterでは十数件のツイートを表示するけど、それ以上過去のはユーザがスクロールしてみようとしたときに始めて表示するようにするあれ。
個人的にはフロントエンドの技術であって、バックエンドで積極的にLazyLoadは使うものではないとは思ってる。
なぜなら、最終的なコストはlazyLoad使ったほうが高くなるので、必要に応じて取得するべきだとは思っている。
コーディングは楽にはなるんだけど…。
詳細原因
基本的にはこの対象のテーブル単体で呼ぶことはない。
「受注」みたいな巨大な基本IDをもつドメインが「受注」から「購入商品」テーブルを、「購入商品」から「商品詳細」テーブルをlazyLoadでデータを取得していた。
その時は、参照性制約のある外部キーで引っ張ってこれるようにしていた。
しかし、主キー項目にidをちゃんと設定していない、かつ、lazyLoadで取得するような処理だと以下のような挙動になる。
- Java側で特定のドメインを呼ぶ
- Mybatisがlazyloadでデータを取得しに行く
- 通常通りに取得できる
- Java側でもう1回特定のドメインを呼ぶ(forループとか)
- Mybatisがlazyloadでデータを取得しに行く
- Mybatisがなぜか気を利かせて主キー項目を自動で採番する
- 3で取得していた項目と主キー項目が被っていたから…?
- 処理終了時には主キー以外が完全に同等のドメインが作成される
このメモリにデータが増えた状態で、最終的にドメインを更新掛けに行ったので、レコードが増えてました。
終わりに
正直、Javaで途中でデータを挿入したとか、検索キーのJoinとかが失敗していて、必要のないデータを取得していた、とかそういう業務ロジックでのやらかしだと思っていたので、かなり調査に時間かかりました。
影響が出た箇所をコメント化して、悪いところを明確にしよう、と思ったら別のロジックでデータ増えるし… データが増えた瞬間を捉えられないと、原因として切り分けられないですし…。
まぁ、最初からちゃんと設計できれば問題ないんですけど。
lazyLoadでやってるから最小限で実装するね!
各々が必要になったときに増やして!
なんてやったものだから、この辺の処理ほんとガバガバでした…。
第二第三の犠牲者が出ないことを祈って、障害を供養します。
Appendix
自分検索用キー - Java - MyBatis - なぜかデータが増える