®️

Redis使ってセッションをDB管理

2024/05/01に公開

Redisを使ってセッション管理

以前、SpringBootとH2DBを使って、セッションをDB管理してみました。今回は、発展形としてRedis(AWS ElastiCache)でセッション管理します。

(前提知識)RDBMSとNoSQL

DBには、大きくRDBMSとNoSQLがあります。

両者のメリットデメリットをまとめると、以下の表になります。

種別 メリット デメリット
RDBMS SQLにより複雑な操作が可能、ACID特性によるデータの安全性の担保 処理速度が遅い、分散管理時にデータの書き込みに不向き
NoSQL 処理が高速、スケールアウトしやすい データの一貫性を保つことができない、複雑な処理には不向き

RDBMS

  • RDBMSは、テーブル形式でデータを格納し、それぞれのテーブルは行と列で構成されます。

  • これにより、データ間の関連性を表現しやすく、SQLという標準的なクエリ言語を用いてデータの操作が可能です。

  • ACID特性※を満たすことで、データの整合性と信頼性を保証します。

    ACID特性は、以下の4つの特性から構成されます。

    • Atomicity(原子性):データベースに関する一連の操作がすべて実行されるか、一つも実行されないかのどちらかとなることを保証します。
    • Consistency(一貫性):データベースの状態が常に一定のルールや制約を満たし、データベーストランザクションの前後でデータベースが正当な状態の維持を保証します。
    • Isolation(独立性):実行中のトランザクションが他のトランザクションの途中結果に影響されないことを保証します。
    • Durability(永続性):システム障害やクラッシュが発生した場合でも、完了したトランザクションの結果が保持され続けることを保証します。

NoSQL

NoSQLは、"Not Only SQL"の略で、従来のリレーショナルデータベースの制約を取り払った新しい形式のデータベースです。

  • 近年のNoSQLは、ほとんどの場合にインメモリで動作するため、ビックデータ等の大量データを効率的に処理することが可能です。
  • トランザクション処理ができない為、RDBの特徴である、ACID特性を満たしません。
  • 非リレーショナルなデータ(例:階層的なデータ(JSONやXMLなど)、キー-値ペア、リストなど)を扱うことができ、扱うデータによりいくつかの種類が存在します。
    • キーバリュー型

      Key Value
      田中 (年齢:30, 性別:男, 勤務先:大阪)
      小林 (性別: 女, 年齢:40, 役職:部長)
    • ドキュメント指向型

      Key Value
      田中 {ID:001, job: farmer, age: 30}
      小林 {ID:002, job: pilot, age: 40}

NoSQLの中のRedis

Redisは、NoSQLのキーバリュー型の一つで、メモリ上でデータを管理するインメモリデータべースです。

一般的にキーバリュー型では、キーに対して文字列値を関連づけてデータを保存しています。それに対して、Redisではキーに対して以下のような複数のデータ型の値を保持することができます。

  • Strings(文字列型)

  • Lists(リスト型)

  • Sets(セット型)

  • Hashes(ハッシュ型)

  • Sorted sets(ソート済セット型)

  • etc…

(参考:https://redis.io/docs/data-types/)

原子性

Redisは、トランザクションをサポートしており、MULTI、EXEC、DISCARD、WATCHコマンドを使用して原子性を実現します。これにより、一連のコマンドがすべてまたは一つも実行されないことを保証します。

しかし、RedisのトランザクションはRDBMSのように複雑なロールバック機能を持っておらず、Redisサーバーがクラッシュしたり、システム管理者によって強制的に終了させられたりした場合、操作の一部だけが登録される可能性があります。Redisは再起動時にこの状態を検出し、エラーで終了します。

(参考:https://redis.io/docs/interact/transactions/)

一貫性

Redis単体はStrongConsistencyをサポートしていません。つまり、クライアント側がデータの一貫性を管理する必要があります。

(参考:https://redis.io/docs/management/scaling/#redis-cluster-101)

独立性

Redisのトランザクションは、WATCHコマンドを使用することで、ある程度の独立性を提供します。これにより、トランザクションが開始されてから実行されるまでの間に監視されているキーが変更された場合、トランザクションは中断されます。しかし、Redisは基本的にシングルスレッドで動作するため、高度な隔離レベルを提供することは限定的です。

(参考:https://redis.io/docs/interact/transactions/#watch-explained)

永続性

メモリ上でデータ管理するインメモリデータベースのデメリットとして、給電がストップすると全てのデータが消失するリスクがあります。Redisではこの欠点を補うため、データを定期的にディスクに保存してデータ損失を避ける仕組みが導入されています。

  • RDB
    • メモリ内のデータのスナップショットを定期的に取り、それをコンパクトなバイナリ形式でディスクに保存するRedisのデフォルトの永続性オプションです。
    • メリット:コンパクトなデータ形式、高速な回復、パフォーマンスへの影響が最小限
    • デメリット:データ損失の可能性、フォーキングオーバーヘッド
  • AOF
    • AOFは、データセットを変更するすべての書き込み操作を追記専用ファイルに記録する代替の永続性オプションです。
    • メリット:より良い耐久性、人間が読める形式、柔軟な設定
    • デメリット:大きなファイルサイズ、回復が遅い

(参考:https://redis.io/docs/management/persistence/)

レプリケーション

Redisには、データの冗長性と可用性を高めるための仕組み(レプリケーション)が導入されています。一つのマスターインスタンスと、一つ以上のレプリカインスタンスが存在します。マスターは、全ての書き込みと読み込みのリクエストを処理します。一方、レプリカは、マスターのデータのコピーを保持し、主に読み込みリクエストの処理に使用されます。

レプリケーションは、「複製(レプリカ)を作ること」を意味し、サーバーやデータベースの複製を作り、負荷分散やホットスタンバイの役割を持たせることを意味します。

これにより、参照先が分散され、負荷分散が実現できます。また、マスターに障害が発生した場合は、レプリカの内の一つがマスターとなり、システムを継続させます。

(参考:https://redis.io/docs/management/replication/#important-facts-about-redis-replication)

シャーディング

メモリ上でデータを管理するインメモリデータベースのデメリットとして、容量が少ないという問題があります。これを解決するために、Redisではシャーディングが導入されます。シャーディングは、データを複数のRedisサーバー(ノード)に分散させる手法です。このアプローチにより、単一のRedisインスタンスのリソース制限を超えるデータを管理することができ、データセットのスケールアップとアプリケーションのパフォーマンス向上が可能になります。

Redisのシャーディングを実現する主な方法の一つが、Redisクラスターです。Redisクラスターはデータをシャード(分割)し、複数のノード間でデータを分散させます。各ノードはslotと呼ばれる識別子が付与されたデータを分散して保持します。

クライアントは、データのキーに基づいて特定のslotを計算し、そのslotを担当するノードに直接アクセスすることでデータの読み書きを行います。

(参考:https://redis.io/docs/management/scaling/)

AWSとRedis

スケーラビリティの容易性や高可用性、耐久性の観点から、AWSのサービスを利用してRedisを使うことができます。AWSが提供するRedis関連のサービスは大きく2つあります。

  • Amazon ElastiCache for Redis
    • 用途
      ElastiCache for Redisは、キャッシング、セッションストア、リーダーボード、地理空間データの処理など、高速な読み書きが必要な一時的なデータの保存に適しています。
    • 耐久性
      データの耐久性に関しては、ElastiCacheは主にインメモリで動作し、オプションでディスクへのスナップショット保存をサポートしていますが、これは主にバックアップ目的であり、MemoryDBほどの高耐久性はありませんが、あくまで比較した場合であり、適切な設定により、高い可用性を提供することができます。
  • Amazon MemoryDB for Redis
    • 用途
      ElastiCacheよりも高い耐久性とデータの永続性が求められるユースケースに適しています。フルマネージドのRedis互換データベースとして、トランザクションログとスナップショットを使ってデータを永続化し、耐久性を確保します。
    • 耐久性
      MemoryDBは、データの耐久性と可用性に重点を置いて設計されています。複数のアベイラビリティーゾーンにわたって自動的にデータをレプリケートし、高い耐久性を提供します。

参考
ElastiCacheについて:https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/red-ug/WhatIs.html
MemoryDBについて:https://docs.aws.amazon.com/ja_jp/memorydb/latest/devguide/what-is-memorydb-for-redis.html
ElastiCache vs MemoryDB:https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/red-ug/related-services-choose-between-memorydb-and-redis.html

実際にやってみる

ここまでが座学のところです。ここから実際にRedisを利用してみます。今回は基盤チームにAmazon ElastiCache for Redisを提供頂いたので、それを活用します。またアプリケーションはSpringBootフレームワークを利用します。

アプリケーション準備

それぞれ以下を記載します。

  • applicaton.properties

    #redisの設定、後ほどアプリ起動時に環境変数で上書きしますが、これがないとビルドエラーになるので記載
    spring.redis.host=hugehuge.host.com
    spring.redis.port=6379
    spring.redis.ssl=true
    
  • pom.xml

    <!-- for redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    
  • Java

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.session.data.redis.config.ConfigureRedisAction;
    import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
    
    /**
     * セッションに関するConfigクラス.
     *
     * @EnableRedisHttpSessionでSpring Sessionの機能を有効化
     * セッション情報の保存先としてRedisを使用することを指定
     * maxInactiveIntervalInSecondsでセッション有効期限(単位:秒)を指定(RedisではなくSpring側の設定)
     *  
     */
    @Configuration
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 180)
    public class SessionConfig {
    
        /**
         * Spring SessionがRedisの設定を変更しないようにします。
         * 
         * AWS ElastiCacheではCONFIGコマンドの使用が制限されており、Spring SessionがRedisの設定を変更しようとするとエラーが発生する場合があります。
         * ConfigureRedisAction.NO_OPを使用することで、これらのエラーを回避します。
         * 
         * 
         * @return {@link ConfigureRedisAction#NO_OP} のインスタンス
         */
        @Bean
        public static ConfigureRedisAction configureRedisAction() {
            return ConfigureRedisAction.NO_OP;
        }
    }
    
  • その他のアプリケーションのコード

    HttpSessionに対してデータを保存する処理をControllerに実装します。

    import javax.servlet.http.HttpSession;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    /**
     * セッションに関する操作を行うコントローラークラスです。
     */
    @Controller
    public class SessionController {
    
        /**
         * 指定された値をセッションに保存します。 
         * 保存する際のキーは現在のスレッドIDを文字列に変換したものを使用します。
         * 
         * @param value セッションに保存する値
         * @param session セッション
         * @return セッション一覧画面のビュー名
         */
        @GetMapping("/session/{value}")
        public String save(@PathVariable("value") String value, HttpSession session) {
            long threadId = Thread.currentThread().getId();
            session.setAttribute(String.valueOf(threadId), value);
            return "sessionList";
        }
    
    }
    

    画面には全てのセッション情報を表示させます。

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Session Contents</title>
    </head>
    <body>
        <h1>Session Contents</h1>
        <ul>
            <!-- Thymeleafのeach構文を使用してセッションの全エントリをループ処理 -->
            <!-- 各エントリの名前と値を表示 -->
            <li th:each="entry : ${#session.getAttributeNames()}" th:text="${entry + ' : ' + #session.getAttribute(entry)}"></li>
        </ul>
    </body>
    </html>
    

Redisを使うのであれば、以上で設定はおしまいです。ただ、今回は複数アプリを立ち上げて、セッションレプリケーションも実現したいので、追加で以下を用意します。

  • Dockerfile
    FROM java8とmavenを利用可能なイメージ
    ARG JAR_FILE=target/*.jar
    COPY ${JAR_FILE} app.jar
    ENTRYPOINT ["java","-jar","/app.jar"]
    
  • docker-compose.yml
    version: '3'
    services:
      app:
        build:
          context: .
          dockerfile: Dockerfile
        ports:
          - "8080:8080"
        environment:
          SPRING_REDIS_HOST: hugehuge.host.com
      app2:
        build:
          context: .
          dockerfile: Dockerfile
        ports:
          - "8081:8080"
        environment:
          SPRING_REDIS_HOST: hugehuge.host.com
    
    SPRING_REDIS_HOSTはAWSのElastiCacheのプライマリエンドポイントを指定します。

アプリケーション実行

docker compose upで実行します。

アプリケーションにアクセスして確認してみます(ドメインは適宜読み替えてください)。

1つ目のアプリでセッション作成

  • 1つ目のアプリケーション(http://~:8080/session/app1_1st) にアクセスします

    urlで指定した文字列(app1_1st)がセッションに格納されています。

2つ目のアプリでセッション作成

  • 2つ目のアプリケーション(http://~:8081/session/app2_1st) にアクセスします

    画面に表示されているのはRedisで管理されている全てのセッション情報です。1つ目のセッション情報が残っており、2つのアプリケーションで同じ情報を参照できています。

Redisの内容確認

redisをGUIで確認するツールもあるようですが、今回は、redis-cli(Redisのコマンドラインインターフェース)を用いて閲覧します。

redis-cli --tls --insecure -h プライマリエンドポイント -p 6379でRedisサーバに接続します(テストのため、証明書の検証はスキップします)。

キーの一覧

続いて、KEYS *でキーの一覧を取得できます。

keys *
1) "spring:session:sessions:expires:51812e2c-a273-44a7-9639-b3b8968b5e4d"
2) "spring:session:sessions:51812e2c-a273-44a7-9639-b3b8968b5e4d"
3) "spring:session:expirations:1711523220000"
  1. "spring:session:sessions:expires:51812e2c-a273-44a7-9639-b3b8968b5e4d"
    このキーは、特定のセッションID(ここでは51812e2c-a273-44a7-9639-b3b8968b5e4d)に対する有効期限情報を保持しています。Spring Sessionは、セッションの有効期限を管理するためにこのようなキーを使用します。セッションが期限切れになると、この情報をもとにセッションが自動的に削除されます。
  2. "spring:session:expirations:1711523220000"
    このキーは、特定の期限切れ時間(タイムスタンプ)に到達したセッションを追跡するために使用されます。この例では、1711523220000はミリ秒単位のUNIXタイムスタンプで、特定の時点を表しています。Spring Sessionは、このキーを使用して、特定の時点で期限切れになるセッションの集合を管理します。
  3. "spring:session:sessions:51812e2c-a273-44a7-9639-b3b8968b5e4d"
    このキーは、セッションID:51812e2c-a273-44a7-9639-b3b8968b5e4dに関連するセッションデータを保持しています。セッションデータには、ユーザー認証情報、セッション属性など、セッション中に保存された様々な情報が含まれます。このキーを使用して、セッションデータにアクセスし、操作することができます。

セッションの内容

続けて、HGETALLでセッションの中身を閲覧します。1つのセッションIDに、画面で確認したセッション情報2つが紐づいていることが確認できます。

HGETALL "spring:session:sessions:51812e2c-a273-44a7-9639-b3b8968b5e4d"
 1) "sessionAttr:25"
 2) "\xac\xed\x00\x05t\x00\bapp1_1st"
 3) "maxInactiveInterval"
 4) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\xb4"
 5) "creationTime"
 6) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x8e~\xb8y\x9b"
 7) "lastAccessedTime"
 8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x8e~\xb8\x89H"
 9) "sessionAttr:24"
10) "\xac\xed\x00\x05t\x00\bapp2_1st"
  1. "sessionAttr:24","sessionAttr:25"
    これらはセッションに保存された属性のキーです。属性の値は、Javaのシリアライズ形式でエンコードされた文字列(例: "\xac\xed\x00\x05t\x00\bapp1_1st")です。これは特定のアプリケーション固有のデータ(この例では app1_1st)を含んでいます。このシリアライズ形式はJavaのオブジェクトをバイト列に変換する標準的な方法です。
  2. "maxInactiveInterval"
    セッションが非活動状態であるとみなされる最大時間(秒単位)を示します。この値はJavaのInteger型のシリアライズ形式で保存されています。
  3. "creationTime"
    セッションの作成時間を示します。値はJavaのLong型のシリアライズ形式で、セッションが作成された時刻のUNIXタイムスタンプを表しています。
  4. "lastAccessedTime"
    セッションが最後にアクセスされた時刻を示します。この値もJavaのLong型のシリアライズ形式で、UNIXタイムスタンプを表しています。

おわりに

高速なパフォーマンス、スケーラビリティ、データの冗長性、柔軟なデータ構造が求められる場合、Redisを使ったセッション管理は有力な候補になると思いました。
また今回は最低限の設定で行いましたが、アプリケーションの要件に応じて、より詳細な設定を行うことが重要だと感じました。
例えば、マスターとレプリカ間のデータ同期の遅延の許容範囲を設定することや、セキュリティ設定(パスワード認証やSSL通信の有効化など)、キーの有効期限設定、バックアップ頻度などです。これらをアプリ側、AWS側に、適切に設定することで、セッションデータの安全性と効率的な管理を行うことができそうです。

Discussion