Javaバックエンド技術面接対策:トランザクション、分散システム、キャッシュ戦略

1. トランザクション処理

1.1 ローカルトランザクション

ローカルトランザクションは通常、Springフレームワークによって管理されます。実装方法としてはプログラマティックな手法と宣言的な手法(`@Transactional`アノテーションなど)がありますが、一般的には宣言的手法が用いられます。Springのトランザクション管理はデータベースのトランザクション機能に依存しており、データベースはACID特性を満たす必要があります。

  • 原子性 (Atomicity): トランザクション内の操作はすべて成功するか、すべて失敗してロールバックされるかのどちらかであり、部分的な実行状態は存在しません。
  • 一貫性 (Consistency): トランザクションの実行前後で、データベースの整合性制約(一意性制約、外部キー制約など)が維持されている必要があります。
  • 分離性 (Isolation): 同時に実行されるトランザクションは互いに干渉せず、それぞれが独立して実行されているかのように振る舞う必要があります。
  • 持続性 (Durability): トランザクションがコミットされた後、その変更はシステム障害が発生しても永続的に保存されます。

1.2 分散トランザクション

分散トランザクションは、異なるサービス、異なるデータベース、異なるネットワークにまたがって処理が完了するトランザクションを指します(例:単一サービスで複数データベースを使用する場合や、複数サービスで単一データベースを使用する場合)。

CAP理論:
分散システムにおいて、一貫性、可用性、分断耐性の3つすべてを同時に完全に満たすことはできません。

  • C (Consistency): どのノードからデータを取得しても、同じデータが返されること(最新であることは保証しない)。
  • A (Availability): 常に何らかの応答が返されること(最新であることは保証しない)。
  • P (Partition Tolerance): 通信障害などでシステムが分割されても、サービスを継続できること。

実際のシステム設計では、要件に応じてCP(一貫性と分断耐性)かAP(可用性と分断耐性)のどちらを優先するかを決定します。例えば、銀行送金には即時の一貫性が必要ですが、返金処理などは少しの遅延が許容される場合があります。

解決策:

  • メッセージキューを活用した最終整合性
  • 分散トランザクションフレームワーク(Seataなど)の使用
  • スケジュールタスク(XXL-Jobなど)による遅延処理や補償処理

2. 分散タスクスケジューリング (XXL-Job)

分散環境におけるタスクスケジューリングの解決策として、XXL-JobはQuartzと比較してよく選択されます。Quartzはクラスタリング機能が弱く、監視画面などが別途必要ですが、XXL-Jobは分散実行に特化しており、Webベースの管理コンソールが標準で付属しているため、監視やメンテナンスが容易です。また、スケジューリング(トリガー)と実行(タスク)が分離されている点も特徴です。

3. 大容量ファイルの分割アップロード

大ファイルをアップロードする際、クライアント側でファイルをチャンク(分割)し、ファイルのMD5ハッシュ値を利用して一意性を確認します。プロセスは以下の通りです。

  1. サーバーにファイルの有無(MD5ベース)と、どのチャンクまでアップロード済みかを問い合わせる。
  2. 欠落しているチャンクのみをアップロードする。
  3. 全チャンクのアップロード完了後、サーバー側でチャンクをマージする。

4. 分散ロックの概要

複数のクライアントが同時にリソース(例:チケットの残数)を更新しようとする際、競合を防ぐためにロックが必要です。Redisを使用した分散ロックの一般的な実装は以下の通りです。

  • Redisの SETNX コマンドを使用して、キーが存在しない場合のみ値を設定する(ロックの取得)。
  • 処理完了後にキーを削除する(ロックの解放)。
  • キーが既に存在する場合はロック取得失敗とみなす(リトライまたはエラー)。

他にも、データベースのユニークインデックスを利用する方法や、RedissonやZookeeperなどのライブラリ・サービスを利用する方法があります。

5. Elasticsearch (ES) の活用

Mappingの主な属性:

  • type: データ型(text, keyword, integer, dateなど)。
  • index: インデックスを作成するかどうか(デフォルトはtrue)。
  • analyzer: 使用するアナライザー(分辞器)。
  • properties: ネストされたフィールド定義。

Java API (RestHighLevelClient) の使用手順:

  1. SearchRequestオブジェクトを生成し、インデックス名を指定する。
  2. request.source()を使用してクエリ(DSL)、ページネーション、ソート、ハイライトなどを構築する。
  3. client.search()を実行してレスポンスを取得する。

6. Docker 基礎コマンド

  • docker pull: イメージをプルする。
  • docker save [options] image: イメージをファイルとして保存する。
  • docker load -i file.tar: ファイルからイメージをロードする。
  • docker ps [options]: 実行中のコンテナを表示する。
  • docker logs -f container_id: コンテナのログを追尾表示する。

Dockerfileは、イメージ構築の手順を記述したスクリプトであり、各命令は新しいイメージレイヤーを作成します。

7. キャッシュの問題と対策

7.1 キャッシュ貫通

存在しないデータ(キャッシュにもDBにもない)への大量アクセスが発生し、リクエストがすべてDBに向かう現象です。

対策:

  1. リクエスト検証: 不正なID形式など、事前にバリデーションを行い却下する。
  2. ブルームフィルター (Bloom Filter): 存在しない要素を高速に判定できるフィルターを使用し、存在しない場合はDBにアクセスしない。
  3. 空のキャッシュ: DBにデータが存在しない場合でも、短いTTL(有効期限)でキャッシュに空値(nullや特殊値)を保存する。
public Product getProductFromCache(String productId) {
    String cacheKey = "product:" + productId;
    String cachedData = redisTemplate.opsForValue().get(cacheKey);

    if (cachedData != null) {
        return "NULL".equals(cachedData) ? null : JSON.parseObject(cachedData, Product.class);
    } else {
        // データベースから取得
        Product product = productRepository.findById(productId);
        
        // データが存在しない場合でも短時間キャッシュする(貫通対策)
        String valueToCache = (product == null) ? "NULL" : JSON.toJSONString(product);
        redisTemplate.opsForValue().set(cacheKey, valueToCache, 5, TimeUnit.MINUTES);
        
        return product;
    }
}

7.2 キャッシュ雪崩

大量のキャッシュキーが同時に期限切れになり、瞬時にDBへ大量のリクエストが集中する現象です。

対策:

  1. TTLのランダム化: 有効期限を固定値にせず、固定値 + ランダムな値に設定し、一斉に切れないようにする。
  2. 同期ロック: キャッシュ再構築時にロックを使用し、DBアクセスを1つのスレッドに限定する。
  3. キャッシュ予熱: リクエストが来る前に、バッチ処理などであらかじめキャッシュを温めておく。
// ランダムな有効期限の設定例
int baseExpire = 300; // 5分
int randomExpire = new Random().nextInt(300); // 0〜5分のランダム
redisTemplate.opsForValue().set(key, value, baseExpire + randomExpire, TimeUnit.SECONDS);

7.3 キャッシュ破産(キャッシュブレイクダウン)

ホットなデータ(アクセスが集中する特定のキー)のキャッシュが期限切れになった瞬間に、大量のリクエストがDBへ殺到する現象です。

対策:

  1. 排他ロック: キャッシュ再構築時、1つのスレッドだけがDBにアクセスし、他のスレッドはロックが解除されるのを待つか、キャッシュを待機する。
  2. ホットキーの永続化: 有効期限を設定せず、バックグラウンドジョブなどで適宜キャッシュを更新する。

8. 分散ロックの実装詳細

8.1 アプリケーションシナリオ

  • 在庫のオーバー販売防止。
  • 分散タスクのスケジューリング(重複実行防止)。
  • 共有リソースへの同時アクセス制御。

8.2 実装方式の比較

  1. データベース: ユニークインデックスを利用し、挿入成功をロック取得とみなす。
  2. Redis: SET key value NX EX max-lock-time コマンドを使用。NXはキーが存在しない場合のみ成功、EXは有効期限設定。
  3. Zookeeper: 一時順序ノードを作成し、最小のノードを作成したスレッドがロックを取得する。

8.3 Redisによる分散ロックとLuaスクリプト

ロックを解放する際、誤って他のスレッドのロックを削除しないように、Luaスクリプトを用いて「値の確認」と「削除」をアトミックに行う必要があります。


-- Luaスクリプト: 値が一致する場合のみ削除
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

8.4 Redissonによる実装

RedissonはNettyベースのRedisクライアントであり、RLockインターフェースを通じて便利な分散ロック機能を提供します。ウォッチドッグ(看門犬)機能により、ロックの有効期限が切れても処理が続いている場合、自動的に有効期限が延長されます。

public Product getProductWithLock(String productId) {
    String cacheKey = "product:" + productId;
    String cachedData = redisTemplate.opsForValue().get(cacheKey);
    
    if (StringUtils.isNotEmpty(cachedData)) {
        return JSON.parseObject(cachedData, Product.class);
    }

    // 各製品IDごとのロックを取得
    RLock lock = redissonClient.getLock("lock:product:" + productId);
    try {
        // ロック取得を試みる
        if (lock.tryLock(10, TimeUnit.SECONDS)) {
            try {
                // ダブルチェック:ロック待ちの間に他のスレッドがキャッシュを更新している可能性がある
                cachedData = redisTemplate.opsForValue().get(cacheKey);
                if (StringUtils.isNotEmpty(cachedData)) {
                    return JSON.parseObject(cachedData, Product.class);
                }
                
                // DBから取得してキャッシュに保存
                Product product = productRepository.findById(productId);
                redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 1, TimeUnit.DAYS);
                return product;
            } finally {
                lock.unlock();
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return null; // またはリトライ
}

9. メッセージキュー (RocketMQ)

9.1 役割

  • ピークシェービング: 瞬間的なアクセス集中を平滑化する。
  • 非同期処理: 処理を非同期化してレスポンス時間を短縮する。
  • システム疎結合: システム間の依存度を下げる。

9.2 トピックと消費モード

  • Clustering (クラスタリング): 負荷分散モード。1つのメッセージはコンシューマグループ内の1つのインスタンスのみが消費する。
  • Broadcasting (ブロードキャスト): 全インスタンスが同じメッセージを消費する。

9.3 信頼性の確保

  • プロデューサ: メッセージ送信の確認や、DBへの永続化後にMQへ送信するトランザクション発行パターン。
  • MQ: トピック、キュー、メッセージ自体の永続化設定(フラッシュディスクポリシー)。
  • コンシューマ: 業務処理成功後にACKを返す。失敗時は再試行。

9.4 冪等性の保証

メッセージの重複配信を防ぐため、コンシューマ側では処理が冪等(何回行われても結果が同じ)であることを保証する必要があります。実装方法としては、メッセージの一意IDをDBに記録し、既存のIDであれば処理をスキップする方法などがあります。

10. Springの仕組み

10.1 IoC (Inversion of Control)

  • BeanFactory: シンプルなDIコンテナ。遅延ロード(インスタンス取得時に生成)がデフォルト。
  • ApplicationContext: エンタープライズ機能を備えた拡張コンテナ。起動時にシングルトンBeanを生成する即時ロードがデフォルト。

10.2 AOP (Aspect Oriented Programming)

AOPは動的プロキシ(JDK動的プロキシまたはCGLIB)を使用して実装されます。

  • JDK動的プロキシ: インターフェースを実装しているクラスに対して適用。リフレクションを使用。
  • CGLIB: クラス自体を継承してサブクラスを動的に生成し、メソッドをオーバーライドする。finalクラスには適用不可。

10.3 Spring Boot自動装配

@EnableAutoConfiguration アノテーションにより、クラスパス内のライブラリ(JAR)をスキャンし、`META-INF/spring.factories` に定義された設定クラスを読み込みます。条件アノテーション(`@ConditionalOnClass`, `@ConditionalOnMissingBean` など)を使用して、必要なBeanのみをコンテナに登録します。

タグ: Java SpringBoot redis RocketMQ Elasticsearch

7月29日 06:35 投稿