【失敗談】MyBatisでNOLOCKと同じことをやりたかった

f:id:excite-kazuki:20220228135312p:plain

はじめに

エキサイト株式会社 バックエンドエンジニアの山縣です。

現在は、既存サービスのリビルド(PHP / BEAR.Saturday → Java / SpringBoot)を担当しています。 SpringBootでDBアクセスをするにあたってMyBatisを利用していますが、MyBatisのMapperではNOLOCKヒントに対応していないため、 簡単なSQL文であったとしてもその都度NOLOCKヒントを付与したクエリーを書く必要があります。 そこで、MyBatisを使用しつつREAD UNCOMMITTEDでSELECT文を実行する方法について考えてみました。

本記事では、上記方法と、それを導入できなかった理由について紹介します。

トランザクション分離レベルを確認する

SQL Serverでは下記を実行することでトランザクション分離レベルを確認することができます。

DBCC USEROPTIONS

現状ではREAD COMMITTEDが設定されていることが確認できました。 この状態でSELECT文を実行すると、行ロックが発生してしまう可能性があります。 そのため、行ロックを回避するためにNOLOCKヒントが付与されたクエリー(= READ UNCOMMITTEDで実行されるクエリー)が多くアプリケーションに存在します。

SQL ServerでNOLOCKヒントを付与する

SQL ServerではNOLOCKヒントを付与することで、READ UNCOMMITTEDでSELECT文を実行することができます。 クエリーは下記のように記述します。

SELECT * FROM articles  WITH(NOLOCK) WHERE article_id = 100;

しかし、MyBatisで自動生成されるMapperにはNOLOCKヒントが付与されません。 そのため、READ UNCOMMITTEEDでSELECT文を実行したいときはMapperアノテーションを使用してクエリーを記述しなくてはなりません。

@Mapper
public interface MyArticleMapper {
  @Select("SELECT * FROM articles  WITH(NOLOCK) WHERE article_id = #{articleId}")
  Optional<Article> getArticle(Long articleId);
}

クエリー実行毎にトランザクション分離レベルを設定する

SQLクエリーで表すと下記のようになります。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN TRANSACTION;

SELECT * FROM articles  WHERE article_id = 100;

COMMIT TRANSACTION;

Javaでは下記のように記述することができます。

@Override
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public Article findArticle(Long articleId) {
    return articleMapper.selectByPrimaryKey(articleId);
}

実際に上記2つを実行してみると、どちらもREAD UNCOMMITTEDでSELECT文を実行することができました。 そのため、リポジトリでクエリーを実行する箇所に@Transactionalアノテーションを付与するとよさそうです。

実行結果

1つのエンドポイントで2つのクエリーを実行している箇所の実行時間を比較した結果、下記の結果を得ることができました。

Webページでは1ページで複数のAPI呼び出しを行っているため、実際にページが表示されるのが何倍にも遅くなってしまうようになりました😨 これだけの性能差があると、クエリー実行毎にトランザクション分離レベルを設定する方法は実運用に耐えられないことがわかりました。

おわりに

いつもはうまく解決できたことを記事にしていますが、今回はうまくいかなかったことについて紹介してみました。 クエリーを実行するときにトランザクション分離レベルを設定するのはコストがかかることなんだと実感できました。 SQL ServerでREAD UNCOMMITTEDでSELECT文を実行するときは、素直にNOLOCKヒントを付与するのがよさそうです。 最後まで読んでいただき、ありがとうございました!