リアクティブ Redis インデックス付き構成

Redis インデックス付き Web セッションサポートの使用を開始するには、次の依存関係をプロジェクトに追加する必要があります。

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
implementation 'org.springframework.session:spring-session-data-redis'

そして、@EnableRedisIndexedWebSession アノテーションを構成クラスに追加します。

@Configuration
@EnableRedisIndexedWebSession
public class SessionConfig {
    // ...
}

それだ。これで、アプリケーションには、リアクティブな Redis でバックアップされたインデックス付き Web セッションのサポートが追加されました。アプリケーションの構成が完了したため、カスタマイズを開始することもできます。

JSON を使用したセッションの直列化

デフォルトでは、Spring Session Data Redis は Java 直列化を使用してセッション属性を直列化します。特に、同じ Redis インスタンスを使用しているが、同じクラスの異なるバージョンを持つ複数のアプリケーションがある場合、問題が発生することがあります。RedisSerializer Bean を提供して、セッションを Redis に直列化する方法をカスタマイズできます。Spring Data Redis は、Jackson の ObjectMapper を使用してオブジェクトを直列化および逆直列化する GenericJackson2JsonRedisSerializer を提供します。

RedisSerializer の構成
@Configuration
public class SessionConfig implements BeanClassLoaderAware {

	private ClassLoader loader;

	/**
	 * Note that the bean name for this bean is intentionally
	 * {@code springSessionDefaultRedisSerializer}. It must be named this way to override
	 * the default {@link RedisSerializer} used by Spring Session.
	 */
	@Bean
	public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
		return new GenericJackson2JsonRedisSerializer(objectMapper());
	}

	/**
	 * Customized {@link ObjectMapper} to add mix-in for class that doesn't have default
	 * constructors
	 * @return the {@link ObjectMapper} to use
	 */
	private ObjectMapper objectMapper() {
		ObjectMapper mapper = new ObjectMapper();
		mapper.registerModules(SecurityJackson2Modules.getModules(this.loader));
		return mapper;
	}

	/*
	 * @see
	 * org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang
	 * .ClassLoader)
	 */
	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		this.loader = classLoader;
	}

}

上記のコードスニペットは Spring Security を使用しているため、Spring Security の Jackson モジュールを使用するカスタム ObjectMapper を作成しています。Spring Security Jackson モジュールが必要ない場合は、アプリケーションの ObjectMapper Bean を挿入して次のように使用できます。

@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(ObjectMapper objectMapper) {
    return new GenericJackson2JsonRedisSerializer(objectMapper);
}

RedisSerializer Bean 名は、Spring Data Redis で使用される他の RedisSerializer Bean と競合しないように、springSessionDefaultRedisSerializer にする必要があります。別の名前を指定した場合、Spring Session はその名前を選択しません。

別の名前空間の指定

複数のアプリケーションが同じ Redis インスタンスを使用することや、セッションデータを Redis に保存されている他のデータから分離しておきたいことは、珍しいことではありません。そのため、Spring Session は、必要に応じて namespace (デフォルトは spring:session) を使用してセッションデータを分離します。

namespace を指定するには、@EnableRedisIndexedWebSession アノテーションの redisNamespace プロパティを設定します。

Specifying a different namespace
@Configuration
@EnableRedisIndexedWebSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
    // ...
}

Spring Session が期限切れのセッションをクリーンアップする方法を理解する

Spring Session は、期限切れのセッションをクリーンアップするために Redis キースペースイベント (英語) に依存します。具体的には、__keyevent@*__:expired および __keyevent@*__:del チャネルに発行されたイベントをリッスンし、破棄されたキーに基づいてセッション ID を解決します。

例として、ID 1234 のセッションがあり、そのセッションが 30 分で期限切れになるように設定されていると想像してみましょう。有効期限に達すると、Redis は有効期限が切れたキーであるメッセージ spring:session:sessions:expires:1234 を含むイベントを __keyevent@*__:expired チャネルに送信します。次に、Spring Session はキーからセッション ID (1234) を解決し、関連するすべてのセッションキーを Redis から削除します。

Redis の有効期限にのみ依存する場合の問題の 1 つは、キーがアクセスされていない場合に、Redis が期限切れイベントがいつ発生するかを保証しないことです。詳細については、Redis ドキュメントの Redis がキーを期限切れにする方法 (英語) を参照してください。期限切れイベントが発生することが保証されていないという事実を回避するために、各キーが期限切れになると予想されるときに確実にアクセスされるようにすることができます。これは、キーの TTL が期限切れになっている場合、Redis はキーを削除し、キーにアクセスしようとすると期限切れイベントを発生させることを意味します。このため、各セッションの有効期限は、有効期限でランク付けされたソートセットにセッション ID を格納することによっても追跡されます。これにより、バックグラウンドタスクが期限切れの可能性があるセッションにアクセスして、Redis 期限切れイベントがより確定的な方法で確実に起動されるようにすることができます。例:

ZADD spring:session:sessions:expirations "1.702402961162E12" "648377f7-c76f-4f45-b847-c0268bb48381"

場合によっては、有効期限が切れていないのにキーが期限切れであると誤って識別する競合状態が発生する可能性があるため、キーを明示的に削除しません。分散ロックを使用しないと(パフォーマンスが低下します)、有効期限マッピングの一貫性を確保する方法はありません。キーにアクセスするだけで、そのキーの TTL が期限切れになった場合にのみキーが削除されるようになります。

デフォルトでは、Spring Session は 60 秒ごとに最大 100 個の期限切れセッションを取得します。クリーンアップタスクの実行頻度を構成する場合は、"セッションクリーンアップの頻度の変更" セクションを参照してください。

キースペースイベントを送信するための Redis の構成

デフォルトでは、Spring Session は ConfigureNotifyKeyspaceEventsReactiveAction を使用してキースペースイベントを送信するように Redis を構成しようとします。これにより、notify-keyspace-events 構成プロパティが Egx に設定される可能性があります。ただし、Redis インスタンスが適切に保護されている場合、この戦略は機能しません。その場合、Redis インスタンスを外部で構成し、ConfigureReactiveRedisAction.NO_OP 型の Bean を公開して自動構成を無効にする必要があります。

@Bean
public ConfigureReactiveRedisAction configureReactiveRedisAction() {
    return ConfigureReactiveRedisAction.NO_OP;
}

セッションクリーンアップの頻度の変更

アプリケーションのニーズに応じて、セッションのクリーンアップの頻度を変更したい場合があります。これを行うには、ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository> Bean を公開し、cleanupInterval プロパティを設定します。

@Bean
public ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository> reactiveSessionRepositoryCustomizer() {
    return (sessionRepository) -> sessionRepository.setCleanupInterval(Duration.ofSeconds(30));
}

disableCleanupTask() の呼び出しを設定してクリーンアップタスクを無効にすることもできます。

@Bean
public ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository> reactiveSessionRepositoryCustomizer() {
    return (sessionRepository) -> sessionRepository.disableCleanupTask();
}

クリーンアップタスクを制御する

場合によっては、デフォルトのクリーンアップタスクではアプリケーションのニーズを十分に満たせない場合があります。期限切れのセッションをクリーンアップするには、別の戦略を採用することもできます。セッション ID はキー spring:session:sessions:expirations にソートされたセットに格納され、有効期限によってランク付けされていることがわかっているためデフォルトのクリーンアップタスクを無効にして独自の戦略を提供できます。例:

@Component
public class SessionEvicter {

    private ReactiveRedisOperations<String, String> redisOperations;

    @Scheduled
    public Mono<Void> cleanup() {
        Instant now = Instant.now();
        Instant oneMinuteAgo = now.minus(Duration.ofMinutes(1));
        Range<Double> range = Range.closed((double) oneMinuteAgo.toEpochMilli(), (double) now.toEpochMilli());
        Limit limit = Limit.limit().count(1000);
        return this.redisOperations.opsForZSet().reverseRangeByScore("spring:session:sessions:expirations", range, limit)
                // do something with the session ids
                .then();
    }

}

セッションイベントのリスニング

多くの場合、セッションイベントに反応することが重要です。たとえば、セッションのライフサイクルに応じて何らかの処理を実行する必要がある場合があります。

SessionCreatedEventSessionDeletedEventSessionExpiredEvent イベントをリッスンするようにアプリケーションを構成します。Spring でアプリケーションイベントをリッスンする方法はいくつかありますが、この例では @EventListener アノテーションを使用します。

@Component
public class SessionEventListener {

    @EventListener
    public Mono<Void> processSessionCreatedEvent(SessionCreatedEvent event) {
        // do the necessary work
    }

    @EventListener
    public Mono<Void> processSessionDeletedEvent(SessionDeletedEvent event) {
        // do the necessary work
    }

    @EventListener
    public Mono<Void> processSessionExpiredEvent(SessionExpiredEvent event) {
        // do the necessary work
    }

}