Springのトランザクション処理について改めて調べた
以下を組み合わせて使うと例外が発生してもDBの更新がrollbackされないという事象に遭遇して、Springのソースコードを改めて読み直したのでメモしておく。
- Kotlin Coroutine
- SpringのTransactionalアノテーション
- MyBatis
今回使用したバージョン
- Kotlin: 1.9.24
- kotlinx-coroutines: 1.8.1
- Spring Boot: 3.3.1
発生した現象と原因
以下のようなコードの場合、
@Service
class FooService(
private val fooRepository: FooRepository
) {
@Transactional
suspend fun upsert() {
// DB更新
fooRepository.update()
}
}
以下のように処理が実行されていた。
coroutineの非同期処理を待ってcommit/rollbackをしてくれない。
トランザクション開始
↓
すぐcommit
↓
suspend関数の非同期処理を実行
Springのソースコード
トランザクション処理は、
org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
で行われている。
- TransactionAspectSupport
- Transactionalアノテーションを付けている関数がsuspendだと、 TransactionAspectSupportの
invocation.proceedWithInvocation()
の戻り値がMonoOnAssemblyになっている- SpringはMonoを使ってCoroutineを処理している
- createTransactionIfNecessary でトランザクション開始
- commitTransactionAfterReturning でcommit
- completeTransactionAfterThrowing でrollback
- Transactionalアノテーションを付けている関数がsuspendだと、 TransactionAspectSupportの
まとめ
Coroutine、SpringのTransactionalアノテーション、MyBatisを組み合わせると、トランザクションが意図したように動作しない理由をSpringのソースコードを読んで確認しました。
対応として以下のような選択肢があると思います。
- R2DBC (Reactive Relational Database Connectivity)に対応したclientライブラリを使用する
- R2DBCの公式ページに対応ライブラリがまとめてあります → https://r2dbc.io/clients/
- Mybatisのような、非同期処理に対応していない(JDBCにしか対応していない)clientライブラリを使いたい場合はCoroutineを使わずに同期処理にする
- 同期処理は処理の間1つのスレッドを専有してしまうので、十分なスレッド数を確保する事を忘れずに。