間違って付与していた@Transactionalを外したらアプリがデッドロックした話

f:id:akagoma:20211229175603p:plain

はじめに

Springbootで久しぶりにハマった。

事の発端~現象発生

@Transactionalとりあえず付けとけ
ServiceがRepositoryへアクセスし、Repositoryを操作する際には@Transactionalを宣言し、トランザクションの壁を定義するのが定石である。
@Transactionalを宣言すると、HikariPoolからConnectionを1つ貰って、TransactionManager経由でRepositoryの実態へアクセスする。
この@Transactionalだが、このHikariPoolからConnectionをもらう処理があるせいで、排他っぽい動作に見えてしまう時がある。
おそらく「@Transactionalを付与すれば、色々いい感じに排他される。」と思ったのだろうか、Repositoryへのアクセスが無い処理にも@Transactionalが付与されまくっていた。
 
Connectionの不足により、タイムアウトが多発した
何でもかんでも@Transactionalを付けていたせいで、困ったことが起きた。
Connectionが不足し始めた。原因を調査すると、Connection不要の処理でもConnectionを取得していることが分かった。
 
@Transactional不要なら外せ
@Transactionalが必要なのは、データベースとの処理をする場合に限定した。
そうすると、また不思議なことが起きた。
 
デッドロック発生
何故か起動処理が完了しないという現象が発生した。
アプリの起動前にServiceA, ServiceB, ServiceCを呼び出していて、ServiceBとServiceCはExecutorServiceを利用して非同期で実行している。

ServiceAから@Transactionalを取ると、なぜかMainスレッドと、ExecutorServiceの子スレッドでデッドロックが起きた。

 

原因解明

  • 子スレッドはMainスレッドを待っている。
  • 親スレッドはExecutorServiceが終わるのを待っている。
  • Service AにTransactionalを付与すると、デッドロックは起きない。
この条件から、Transactionalの準備ができるのはMainスレッドのみで、ExecutorServiceで実行した子スレッドは準備することができないのではないか。という理由にたどり着いた。

対処

だが、データベース処理が不要なクラスの@Transactionalを付けてとりあえずの対処をするのは信条に反するので、
今回はExecutorServiceを利用しない、というところに行き着いた。

所感

ここ最近、実装でハマったことはなかった。
OSSだからよしなにやってくれるだろう、と甘えていた部分だったので、この動きを理解できてよかった。
Copyright (C) 2018-2022 akagoma. All Rights Reserved.