Reactive Redis Indexed Configurations
要开始使用 Redis 索引 Web 会话支持,需要将以下依赖项添加到项目中:
To start using the Redis Indexed Web Session support, you need to add the following dependency to your project:
-
Maven
-
Gradle
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
implementation 'org.springframework.session:spring-session-data-redis'
并将 @EnableRedisIndexedWebSession
注释添加到配置类:
And add the @EnableRedisIndexedWebSession
annotation to a configuration class:
@Configuration
@EnableRedisIndexedWebSession
public class SessionConfig {
// ...
}
就这样。您的应用程序现在具有反应式 Redis 支持的索引 Web 会话支持。既然您已经配置了应用程序,您可能希望开始自定义设置:
That is it. Your application now has a reactive Redis backed Indexed Web Session support. Now that you have your application configured, you might want to start customizing things:
-
I want to serializing-session-using-json.
-
I want to using-a-different-namespace for keys used by Spring Session.
-
I want to know how-spring-session-cleans-up-expired-sessions.
-
I want to taking-control-over-the-cleanup-task.
-
I want to listening-session-events.
Serializing the Session using JSON
默认情况下,Spring Session Data Redis 使用 Java 序列化来序列化会话属性。有时这可能会出现问题,特别是在具有多个应用程序使用相同 Redis 实例但同一类的版本不同的情况下。您可以提供一个 RedisSerializer
bean 以自定义会话序列化到 Redis 的方式。Spring Data Redis 提供 GenericJackson2JsonRedisSerializer
,它使用 Jackson 的 ObjectMapper
序列化和反序列化对象。
By default, Spring Session Data Redis uses Java Serialization to serialize the session attributes.
Sometimes it might be problematic, especially when you have multiple applications that use the same Redis instance but have different versions of the same class.
You can provide a RedisSerializer
bean to customize how the session is serialized into Redis.
Spring Data Redis provides the GenericJackson2JsonRedisSerializer
that serializes and deserializes objects using Jackson’s ObjectMapper
.
Unresolved include directive in modules/ROOT/pages/configuration/reactive-redis-indexed.adoc - include::example$spring-session-samples/spring-session-sample-boot-redis-json/src/main/java/sample/config/SessionConfig.java[]
上面的代码片段使用的是 Spring Security,因此我们正在创建一个自定义 ObjectMapper
,它使用 Spring Security 的 Jackson 模块。如果您不需要 Spring Security Jackson 模块,则可以注入应用程序的 ObjectMapper
bean 并像这样使用它:
The above code snippet is using Spring Security, therefore we are creating a custom ObjectMapper
that uses Spring Security’s Jackson modules.
If you do not need Spring Security Jackson modules, you can inject your application’s ObjectMapper
bean and use it like so:
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(ObjectMapper objectMapper) {
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
The |
Specifying a Different Namespace
拥有多个应用程序使用相同的 Redis 实例或希望将会话数据与存储在 Redis 中的其他数据分开并不罕见。因此,Spring Session 使用 namespace
(默认为 spring:session
)来在需要时将会话数据分开。
It is not uncommon to have multiple applications that use the same Redis instance or to want to keep the session data separated from other data stored in Redis.
For that reason, Spring Session uses a namespace
(defaults to spring:session
) to keep the session data separated if needed.
您可以通过在 @EnableRedisIndexedWebSession
注释中设置 redisNamespace
属性来指定 namespace
:
You can specify the namespace
by setting the redisNamespace
property in the @EnableRedisIndexedWebSession
annotation:
@Configuration
@EnableRedisIndexedWebSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
// ...
}
Understanding How Spring Session Cleans Up Expired Sessions
Spring Session 依赖于 Redis Keyspace Events 来清理已过期的会话。更具体地说,它监听 keyevent@*:expired
和 keyevent@*:del
通道发出的事件,并根据被销毁的键解析会话 ID。
Spring Session relies on Redis Keyspace Events to clean up expired sessions.
More specifically, it listens to events emitted to the keyevent@*:expired
and keyevent@*:del
channels and resolve the session id based on the key that was destroyed.
比如,我们设想一个带有 ID 1234
的会话,会话将在 30 分钟内到期。当达到到期时间时,Redis 会向 keyevent@*:expired
通道发出一个事件,消息为 spring:session:sessions:expires:1234
(已过期的键)。Spring Session 随后会从该键解析会话 ID (1234
) 并从 Redis 删除所有相关的会话键。
As an example, let’s imagine that we have a session with id 1234
and that the session is set to expire in 30 minutes.
When the expiration time is reached, Redis will emit an event to the keyevent@*:expired
channel with the message spring:session:sessions:expires:1234
which is the key that expired.
Spring Session will then resolve the session id (1234
) from the key and delete all the related session keys from Redis.
仅依赖 Redis 过期存在的一个问题在于,如果密钥未被访问,Redis 无法保证何时触发过期事件。要了解更详细的信息,请参阅 Redis 文档中的 How Redis expires keys。为了规避过期事件无法保证发生的情况,我们可以确保在预期过期时访问每个键。这意味着如果键上的 TTL 已过期,Redis 将删除该键并在我们尝试访问该键时触发过期事件。出于此原因,还会通过将会话 ID 存储在一个按其过期时间排名的已排序集合中来跟踪每个会话的过期情况。这样,后台任务就可以访问可能已过期的会话,以确保 Redis 过期事件以更确定性的方式触发。例如:
One problem with relying on Redis expiration exclusively is that Redis makes no guarantee of when the expired event will be fired if the key has not been accessed. For additional details see How Redis expires keys in the Redis documentation. To circumvent the fact that expired events are not guaranteed to happen we can ensure that each key is accessed when it is expected to expire. This means that if the TTL is expired on the key, Redis will remove the key and fire the expired event when we try to access the key. For this reason, each session expiration is also tracked by storing the session id in a sorted set ranked by its expiration time. This allows a background task to access the potentially expired sessions to ensure that Redis expired events are fired in a more deterministic fashion. For example:
ZADD spring:session:sessions:expirations "1.702402961162E12" "648377f7-c76f-4f45-b847-c0268bb48381"
我们不会明确删除键,因为在某些情况下可能存在竞态条件,错误地将尚未过期的键识别为已过期。除了使用分布式锁(这会损害我们的性能)之外,没有办法确保过期映射的一致性。通过简单地访问该键,我们确保在该键的 TTL 已过期的情况下才删除该键。
We do not explicitly delete the keys since in some instances there may be a race condition that incorrectly identifies a key as expired when it is not. Short of using distributed locks (which would kill our performance) there is no way to ensure the consistency of the expiration mapping. By simply accessing the key, we ensure that the key is only removed if the TTL on that key is expired.
默认情况下,Spring Session 每 60 秒获取多达 100 个已过期的会话。如果您想配置清理任务运行的频率,请参考 Changing the Frequency of the Session Cleanup 部分。
By default, Spring Session will retrieve up to 100 expired sessions every 60 seconds. If you want to configure how often the cleanup task runs, please refer to the changing-the-frequency-of-the-session-cleanup section.
Configuring Redis to Send Keyspace Events
默认情况下,Spring Session 尝试将 Redis 配置为使用 ConfigureNotifyKeyspaceEventsReactiveAction
发送键空间事件,之后该事件可能将 notify-keyspace-events
配置属性设置为 Egx
。但是,如果 Redis 实例已正确保护,此策略将不起作用。在这种情况下,Redis 实例应在外部进行配置,并应公开类型为 ConfigureReactiveRedisAction.NO_OP
的 Bean 以禁用自动配置。
By default, Spring Session tries to configure Redis to send keyspace events using the ConfigureNotifyKeyspaceEventsReactiveAction
which, in turn, might set the notify-keyspace-events
configuration property to Egx
.
However, this strategy will not work if the Redis instance has been properly secured.
In that case, the Redis instance should be configured externally and a Bean of type ConfigureReactiveRedisAction.NO_OP
should be exposed to disable the autoconfiguration.
@Bean
public ConfigureReactiveRedisAction configureReactiveRedisAction() {
return ConfigureReactiveRedisAction.NO_OP;
}
Changing the Frequency of the Session Cleanup
根据您应用程序的需求,您可能希望更改会话清理频率。为此,您可以公开 ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository>
Bean 并设置 cleanupInterval
属性:
Depending on your application’s needs, you might want to change the frequency of the session cleanup.
To do that, you can expose a ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository>
bean and set the cleanupInterval
property:
@Bean
public ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository> reactiveSessionRepositoryCustomizer() {
return (sessionRepository) -> sessionRepository.setCleanupInterval(Duration.ofSeconds(30));
}
您还可以设置调用 disableCleanupTask()
来禁用清理任务。
You can also set invoke disableCleanupTask()
to disable the cleanup task.
@Bean
public ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository> reactiveSessionRepositoryCustomizer() {
return (sessionRepository) -> sessionRepository.disableCleanupTask();
}
Taking Control Over the Cleanup Task
有时,默认清理任务可能不足以满足您应用程序的需求。您可能希望采用不同的策略来清理过期的会话。因为您知道 session ids are stored in a sorted set under the key spring:session:sessions:expirations
and ranked by their expiration time,所以可以 disable the default cleanup 任务并提供自己的策略。例如:
Sometimes, the default cleanup task might not be enough for your application’s needs. You might want to adopt a different strategy to clean up expired sessions. Since you know that the how-spring-session-cleans-up-expired-sessions, you can changing-the-frequency-of-the-session-cleanup task and provide your own strategy. For example:
@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();
}
}
Listening to Session Events
通常,对会话事件做出反应很有价值,例如,您可能希望根据会话生命周期进行某种处理。
Often times it is valuable to react to session events, for example, you might want to do some kind of processing depending on the session lifecycle.
您配置应用程序以监听 SessionCreatedEvent
、SessionDeletedEvent
和 SessionExpiredEvent
事件。Spring 中有 few ways to listen to application events,对于此示例,我们将使用 @EventListener
注释。
You configure your application to listen to SessionCreatedEvent
, SessionDeletedEvent
and SessionExpiredEvent
events.
There are a few ways to listen to application events in Spring, for this example we are going to use the @EventListener
annotation.
@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
}
}