はじめに
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だからよしなにやってくれるだろう、と甘えていた部分だったので、この動きを理解できてよかった。