Reactive Redis 索引配置

若要開始使用 Redis 索引 Web Session 支援,您需要將以下相依性新增至您的專案

  • Maven

  • Gradle

<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 Session 支援。既然您已配置好應用程式,您可能想要開始自訂一些項目

使用 JSON 序列化 Session

預設情況下,Spring Session Data Redis 使用 Java 序列化來序列化 Session 屬性。有時這可能會造成問題,尤其是在您有多個應用程式使用相同的 Redis 執行個體,但具有相同類別的不同版本時。您可以提供 RedisSerializer Bean 來客製化 Session 如何序列化至 Redis。Spring Data Redis 提供了 GenericJackson2JsonRedisSerializer,它使用 Jackson 的 ObjectMapper 來序列化和還原序列化物件。

配置 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,因此我們正在建立一個自訂的 ObjectMapper,它使用 Spring Security 的 Jackson 模組。如果您不需要 Spring Security Jackson 模組,您可以注入您應用程式的 ObjectMapper Bean 並像這樣使用它

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

RedisSerializer Bean 名稱必須是 springSessionDefaultRedisSerializer,這樣它才不會與 Spring Data Redis 使用的其他 RedisSerializer Bean 衝突。如果提供了不同的名稱,Spring Session 將不會選取它。

指定不同的命名空間

有多個應用程式使用相同的 Redis 執行個體,或者想要將 Session 資料與儲存在 Redis 中的其他資料分開是很常見的。因此,Spring Session 使用 namespace (預設為 spring:session) 在需要時保持 Session 資料分隔。

您可以透過在 @EnableRedisIndexedWebSession 註解中設定 redisNamespace 屬性來指定 namespace

指定不同的命名空間
@Configuration
@EnableRedisIndexedWebSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
    // ...
}

了解 Spring Session 如何清除過期的 Session

Spring Session 依賴 Redis Keyspace Events 來清除過期的 Session。更具體地說,它監聽發送到 __keyevent@*__:expired__keyevent@*__:del 通道的事件,並根據已銷毀的索引鍵解析 Session ID。

舉例來說,假設我們有一個 Session ID 為 1234 的 Session,並且該 Session 設定為在 30 分鐘後過期。當達到過期時間時,Redis 將會發送一個事件到 __keyevent@*__:expired 通道,訊息為 spring:session:sessions:expires:1234,這是過期的索引鍵。然後 Spring Session 將會從索引鍵解析 Session ID (1234),並從 Redis 刪除所有相關的 Session 索引鍵。

完全依賴 Redis 過期的一個問題是,如果索引鍵未被存取,Redis 無法保證何時會觸發過期事件。如需更多詳細資訊,請參閱 Redis 文件中的 Redis 如何使索引鍵過期。為了規避過期事件不保證發生的事實,我們可以確保在預期索引鍵過期時存取每個索引鍵。這表示如果索引鍵上的 TTL 過期,當我們嘗試存取索引鍵時,Redis 將會移除索引鍵並觸發過期事件。因此,每個 Session 過期也會透過將 Session ID 儲存在一個依過期時間排序的排序集中來追蹤。這允許背景任務存取可能過期的 Session,以確保 Redis 過期事件以更具確定性的方式觸發。例如

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

我們不會明確地刪除索引鍵,因為在某些情況下,可能會發生競爭條件,錯誤地將索引鍵識別為已過期,但實際上並未過期。除了使用分散式鎖定 (這會嚴重影響我們的效能) 之外,沒有辦法確保過期對應的一致性。透過簡單地存取索引鍵,我們確保只有在該索引鍵上的 TTL 過期時才會移除該索引鍵。

預設情況下,Spring Session 每 60 秒將會檢索最多 100 個過期的 Session。如果您想要配置清理任務的執行頻率,請參閱變更 Session 清理的頻率章節。

配置 Redis 以發送 Keyspace 事件

預設情況下,Spring Session 嘗試使用 ConfigureNotifyKeyspaceEventsReactiveAction 配置 Redis 以發送 Keyspace 事件,這反過來可能會將 notify-keyspace-events 配置屬性設定為 Egx。但是,如果 Redis 執行個體已正確保護,則此策略將無法運作。在這種情況下,Redis 執行個體應在外部配置,並且應公開類型為 ConfigureReactiveRedisAction.NO_OP 的 Bean 以停用自動配置。

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

變更 Session 清理的頻率

根據您的應用程式需求,您可能想要變更 Session 清理的頻率。若要執行此操作,您可以公開 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();
}

掌控清理任務

有時,預設的清理任務可能不足以滿足您應用程式的需求。您可能想要採用不同的策略來清理過期的 Session。由於您知道Session 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();
    }

}

監聽 Session 事件

通常,對 Session 事件做出反應很有價值,例如,您可能想要根據 Session 生命週期執行某種處理。

您可以配置您的應用程式以監聽 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
    }

}