先日遭遇した深い落とし穴を紹介します。
以下のようなシステムを開発している方は同様の問題がないかチェックしてください。
AOPでトランザクションを管理している
一部のマスタテーブルをDBから読み込んだあとシステム内でキャッシュしている
マスタメンテナンス画面などで更新があった場合にキャッシュをクリアしている
問題となるコード
Springや、SeasarといったDIコンテナなどに付随するAOPの機能を用いてトランザクション管理していると、
DBをいじる部分のメソッド内ではcommitとかrollbackとか記述する必要がありません。
これは非常に便利で、人的なコーディングでのcommit漏れ、rollback漏れがないので
バグも入り込みにくく効率のよいものです。
私のやっているプロジェクトではSpringframeworkのAOPの機能を用いてトランザクション管理を行っています。
そして、システム内でよく使われ、更新頻度の低いUserテーブルは、DBアクセスのオーバーヘッドを軽減するために
システム内でキャッシュしているのです。
/** ユーザのキャッシュ */
Map<Integer, User> cache;
/** ユーザ取得処理 */
public User getUser(int userId) {
synchronized (this.cache) {
// キャッシュにあればキャッシュされたUserを返す
if (this.cache.containsKey(userId)) {
return this.cache.get(userId);
}
// なければDBから読み込む
return this.userDao.getUser(userId);
}
}
/** キャッシュの削除処理 */
public void clearCache(int userId) {
synchronized (this.cache) {
this.cache.remove(userId);
}
}
キャッシュの実装は上記のようなもので、内部で同期をとってあります。
そして、UserのUPDATE処理では
public void update (User user) {
// UPDATE
this.userDao.update(user);
// キャッシュから更新対象のUserを削除
// 次回Userを取得しようとした時にキャッシュに最新のUserが読み込まれる
clearCache(user.getUserId());
}
といった作りにしていたのです。このupdateメソッドに対してAOPでトランザクションがかけられるわけですね。
Commit前、キャッシュクリア後の魔
前置きが長くなりました。問題が発生するのはこのupdateの際のキャッシュのクリアの後です。
並列して走っているスレッドがあり、update後のキャッシュクリア後、しかしDBのcommitが完了する前のタイミングで
getUser()してしまったのです。
すると、DBはcommitされる前ですから、更新前のUser情報が読み込まれ、キャッシュには古い情報が残り続けるというわけ。
DBのトランザクションとキャッシュクリアは不可分に行わなければならないのです。
しかし、AOPでトランザクションを管理している場合、メソッド境界を跨ぐことになるので
synchronized文で容易にくくってやることができない…。
わざわざキャッシュ管理している場所だけ特殊なトランザクション処理を別途書いてAOPを適用してやる必要が出てきてしまいました。
似たようなシステム構成をしている方、潜在的なバグを孕んでいる可能性があります。早急にチェックするほうがよいでしょう。
投稿日時 : 2007年10月6日 15:08