Javaにおけるデータベース接続とトランザクション、スレッドの関係性

SpringはJava界の代表的なフレームワークであり、現在ではコンテナ管理の事実上の標準となっている。IoCやAOPに加え、特に重要な機能として「データベーストランザクション管理」が挙げられる。これを理解するには、いくつかの基礎概念を押さえる必要がある。本シリーズでは以下の観点から深掘りしていく:

  • java.sql.Connectionの特性、トランザクション表現、およびJavaスレッドとの密接な関係
  • データベースの分離レベルと伝播動作
  • Springが接続プールとトランザクションをどう抽象化・設計しているか
  • Springトランザクションの内部実装メカニズム

今回はその第一弾として、「java.sql.Connection」オブジェクトの本質、トランザクション制御、そしてJavaスレッドとの関係性を解説する。これらの基礎を理解すれば、Springのトランザクション機構も自然と腑に落ちるようになる。

1. Javaにおけるトランザクション制御の基本単位:Connection

Javaでは、java.sql.Connectionインスタンスを通じてデータベースとのセッションを確立し、TCP/IP通信でSQLを送信する。このオブジェクトを使ってトランザクション境界(commit/rollback)を制御する。

「Connectionをnewして使えばいいのでは?」と考える人もいるかもしれない。しかし、それは非効率かつ危険である。なぜなら、Connectionはデータベース側の貴重なリソースだからだ。

1.1 Connectionは高コストなリソース

データベースは各Connectionに対して専用のスレッドやメモリを割り当てる。接続数が増えれば、以下のような負荷がかかる:

リソース種別影響内容
スレッド数スレッド切り替え頻度が上がり、CPU負荷増加
接続生成コスト初期化処理に時間とメモリを消費
メモリ使用量各Connectionの維持にメモリが必要
ロック競合同時更新によるロック管理コスト増大

つまり、無制限にConnectionを作成すると、DBサーバーの性能ボトルネックを引き起こす可能性が高い。

1.2 MySQLの最大接続数設定例

-- 現在の最大接続数を確認
SHOW VARIABLES LIKE 'max_connections';

-- 実行中に一時的に変更(再起動でリセット)
SET GLOBAL max_connections = 200;

-- 永続化するには my.cnf/my.ini に記述
max_connections = 200;

接続数を増やせば良いわけではない。スレッド過多によるコンテキストスイッチングやロック競合が発生し、逆にパフォーマンスが劣化することがある。適切な値は業務負荷に応じてチューニングすべきである。

2. Connectionの二大特性:直列実行と再利用可能

Connectionには次の2つの重要な性質がある:

  • 直列実行性:同一Connection上で複数トランザクションを同時に実行することはできない。必ず前のトランザクション終了後に次の開始となる。
  • 再利用可能:一度確立したConnectionは明示的にcloseされるまで、何度でもSQLやトランザクション操作に使える(TCPセッション維持)。

この「再利用可能」という特性こそ、コネクションプールの設計思想の根幹となっている。

3. JavaスレッドとConnectionの最適な関係とは?

3.1 スレッドがConnectionを占有するのは非効率

理論上、1スレッドが1Connectionをライフタイムにわたって占有することは可能だが、現実的ではない理由が2つある:

  • アプリケーションスレッド数 > DB最大接続数 → 接続待ちが発生
  • スレッドのDBアクセス時間比率は極めて低い → 大半の時間、Connectionが遊んでいる

よって、一般的な設計原則は:
「DB操作が必要な瞬間だけConnectionを借り、即座に返却する」

3.2 複数スレッドが同一Connectionを共有する問題

複数スレッドが同一Connectionを同時に使うと、トランザクションが混ざり合い、意図しないロールバックやコミット漏れが発生する。

// 問題のある例:Thread#1とThread#2が同じConnectionを共有
Thread#1: UPDATE users ... ; COMMIT;
Thread#2: DELETE logs ... ; ROLLBACK;
↓ 実際の実行順序が混ざると…
DELETE logs ... ; UPDATE users ... ; ROLLBACK; ← Thread#1の更新が巻き添えで消える!

3.3 解決策:排他制御 + 明示的なトランザクション終了

同期制御と例外時の明示的ロールバックを組み合わせる必要がある。

Connection conn = getConnection();

// スレッド1
synchronized(conn) {
    try {
        executeUpdate("UPDATE ...");
        executeUpdate("INSERT ...");
        conn.commit();
    } catch (Exception e) {
        conn.rollback(); // 必ずロールバック
        throw e;
    }
}

// スレッド2
synchronized(conn) {
    try {
        executeDelete("DELETE ...");
        conn.rollback();
    } catch (Exception e) {
        conn.rollback();
        throw e;
    }
}

必須ルール:

  1. Connectionへのアクセスは排他的に行う(synchronizedなど)
  2. 例外発生時も含め、トランザクションは必ずcommitまたはrollbackで終える

4. トランザクション終了後、Connectionは破棄すべきか?

答えはNO。Connectionの生成コストは高く(0.1秒以上)、毎回作り直すのは非効率。代わりに「コネクションプール」に返却し、再利用するのが標準的なアプローチである。

5. コネクションプール —— Connectionのライフサイクル管理器

プールの主な機能:

  1. Connectionのプール管理(上限数設定付き)
  2. 要求に対する公平な割り当て(待機キュー対応)
  3. アイドル接続の自動削除 / 高負荷時の動的増加

代表的な実装:HikariCP、Alibaba Druid、Apache DBCP など。

小技:
PROPAGATION_NOT_SUPPORTED を使うと、トランザクション不要な複数DB操作で接続消費を抑えられる(ThreadLocal経由で既存接続を再利用)。

タグ: Java Spring データベーストランザクション

7月26日 21:19 投稿