Redis 配置
现在您已经配置了应用程序,您可能希望开始自定义内容:
-
我想要使用 Spring Boot 属性自定义 Redis 配置
-
我需要帮助进行选择
RedisSessionRepository
或RedisIndexedSessionRepository
. -
我想指定一个不同的命名空间。
-
我想知道会话何时创建、删除、销毁或过期。
-
自定义会话过期存储
使用 JSON 序列化 Session
默认情况下,Spring Session 使用 Java 序列化来序列化会话属性。
有时可能会出现问题,尤其是当您有多个应用程序使用同一个 Redis 实例但具有同一类的不同版本时。
您可以提供RedisSerializer
bean 来自定义如何将会话序列化为 Redis。
Spring Data Redis 提供了GenericJackson2JsonRedisSerializer
它使用 Jackson 的ObjectMapper
.
@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);
}
指定不同的命名空间
多个应用程序使用同一个 Redis 实例的情况并不少见。
因此,Spring Session 使用namespace
(默认为spring:session
) 以将会话数据分开。
使用 Spring Boot 属性
您可以通过设置spring.session.redis.namespace
财产。
spring.session.redis.namespace=spring:session:myapplication
spring:
session:
redis:
namespace: "spring:session:myapplication"
使用 Annotation 的属性
您可以指定namespace
通过设置redisNamespace
属性在@EnableRedisHttpSession
,@EnableRedisIndexedHttpSession
或@EnableRedisWebSession
附注:
@Configuration
@EnableRedisHttpSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
// ...
}
@Configuration
@EnableRedisIndexedHttpSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
// ...
}
@Configuration
@EnableRedisWebSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
// ...
}
选择之间RedisSessionRepository
和RedisIndexedSessionRepository
使用 Spring Session Redis 时,你可能必须在RedisSessionRepository
和RedisIndexedSessionRepository
.
两者都是SessionRepository
在 Redis 中存储会话数据的接口。
但是,它们在处理会话索引和查询的方式上有所不同。
-
RedisSessionRepository
:RedisSessionRepository
是一种基本实现,可将会话数据存储在 Redis 中,而无需任何其他索引。 它使用简单的键值结构来存储会话属性。 每个会话都分配有一个唯一的会话 ID,会话数据存储在与该 ID 关联的 Redis 密钥下。 当需要检索会话时,存储库使用会话 ID 查询 Redis 以获取关联的会话数据。 由于没有索引,因此根据会话 ID 以外的属性或条件查询会话可能效率低下。 -
RedisIndexedSessionRepository
:RedisIndexedSessionRepository
是一种扩展实现,可为 Redis 中存储的会话提供索引功能。 它在 Redis 中引入了额外的数据结构,以根据属性或条件高效地查询会话。 除了RedisSessionRepository
,它会维护其他索引以实现快速查找。 例如,它可能会根据会话属性(如用户 ID 或上次访问时间)创建索引。 这些索引允许根据特定条件高效查询会话,从而提高性能并启用高级会话管理功能。 除此之外,RedisIndexedSessionRepository
还支持会话过期和删除。
使用RedisIndexedSessionRepository 使用 Redis 集群时,您必须注意,它仅订阅来自集群中一个随机 Redis 节点的事件,如果事件发生在不同的节点中,这可能会导致某些会话索引无法清理。 |
配置RedisSessionRepository
侦听会话事件
配置索引存储库后,您现在可以开始侦听SessionCreatedEvent
,SessionDeletedEvent
,SessionDestroyedEvent
和SessionExpiredEvent
事件。
在 Spring 中有几种方法可以监听应用程序事件,我们将使用@EventListener
注解。
@Component
public class SessionEventListener {
@EventListener
public void processSessionCreatedEvent(SessionCreatedEvent event) {
// do the necessary work
}
@EventListener
public void processSessionDeletedEvent(SessionDeletedEvent event) {
// do the necessary work
}
@EventListener
public void processSessionDestroyedEvent(SessionDestroyedEvent event) {
// do the necessary work
}
@EventListener
public void processSessionExpiredEvent(SessionExpiredEvent event) {
// do the necessary work
}
}
查找特定用户的所有会话
通过检索特定用户的所有会话,您可以跨设备或浏览器跟踪用户的活动会话。 例如,您可以使用此信息进行会话管理,例如允许用户使特定会话无效或注销,或者根据用户的会话活动执行作。
为此,首先您必须使用索引存储库,然后您可以注入FindByIndexNameSessionRepository
接口,如下所示:
@Autowired
public FindByIndexNameSessionRepository<? extends Session> sessions;
public Collection<? extends Session> getSessions(Principal principal) {
Collection<? extends Session> usersSessions = this.sessions.findByPrincipalName(principal.getName()).values();
return usersSessions;
}
public void removeSession(Principal principal, String sessionIdToDelete) {
Set<String> usersSessionIds = this.sessions.findByPrincipalName(principal.getName()).keySet();
if (usersSessionIds.contains(sessionIdToDelete)) {
this.sessions.deleteById(sessionIdToDelete);
}
}
在上面的示例中,您可以使用getSessions
方法查找特定用户的所有会话,并使用removeSession
方法删除用户的特定会话。
配置 Redis 会话映射器
Spring Session Redis 从 Redis 中检索会话信息并将其存储在Map<String, Object>
.
此映射需要经过映射过程才能转换为MapSession
object,然后在RedisSession
.
用于此目的的默认映射器称为RedisSessionMapper
.
如果会话映射不包含构建会话所需的最小键,例如creationTime
,此 mapper 将引发异常。
缺少所需密钥的一种可能情况是,在保存过程正在进行时,会话密钥通常由于过期而被同时删除。
发生这种情况是因为 HSET 命令用于设置键中的字段,如果键不存在,此命令将创建它。
如果要自定义映射过程,可以创建BiFunction<String, Map<String, Object>, MapSession>
并将其设置到 Session 存储库中。
以下示例显示了如何将映射过程委托给默认映射器,但如果引发异常,则会从 Redis 中删除该会话:
-
RedisSessionRepository
-
RedisIndexedSessionRepository
-
ReactiveRedisSessionRepository
@Configuration
@EnableRedisHttpSession
public class SessionConfig {
@Bean
SessionRepositoryCustomizer<RedisSessionRepository> redisSessionRepositoryCustomizer() {
return (redisSessionRepository) -> redisSessionRepository
.setRedisSessionMapper(new SafeRedisSessionMapper(redisSessionRepository));
}
static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, MapSession> {
private final RedisSessionMapper delegate = new RedisSessionMapper();
private final RedisSessionRepository sessionRepository;
SafeRedisSessionMapper(RedisSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public MapSession apply(String sessionId, Map<String, Object> map) {
try {
return this.delegate.apply(sessionId, map);
}
catch (IllegalStateException ex) {
this.sessionRepository.deleteById(sessionId);
return null;
}
}
}
}
@Configuration
@EnableRedisIndexedHttpSession
public class SessionConfig {
@Bean
SessionRepositoryCustomizer<RedisIndexedSessionRepository> redisSessionRepositoryCustomizer() {
return (redisSessionRepository) -> redisSessionRepository.setRedisSessionMapper(
new SafeRedisSessionMapper(redisSessionRepository.getSessionRedisOperations()));
}
static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, MapSession> {
private final RedisSessionMapper delegate = new RedisSessionMapper();
private final RedisOperations<String, Object> redisOperations;
SafeRedisSessionMapper(RedisOperations<String, Object> redisOperations) {
this.redisOperations = redisOperations;
}
@Override
public MapSession apply(String sessionId, Map<String, Object> map) {
try {
return this.delegate.apply(sessionId, map);
}
catch (IllegalStateException ex) {
// if you use a different redis namespace, change the key accordingly
this.redisOperations.delete("spring:session:sessions:" + sessionId); // we do not invoke RedisIndexedSessionRepository#deleteById to avoid an infinite loop because the method also invokes this mapper
return null;
}
}
}
}
@Configuration
@EnableRedisWebSession
public class SessionConfig {
@Bean
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> redisSessionRepositoryCustomizer() {
return (redisSessionRepository) -> redisSessionRepository
.setRedisSessionMapper(new SafeRedisSessionMapper(redisSessionRepository));
}
static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, Mono<MapSession>> {
private final RedisSessionMapper delegate = new RedisSessionMapper();
private final ReactiveRedisSessionRepository sessionRepository;
SafeRedisSessionMapper(ReactiveRedisSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public Mono<MapSession> apply(String sessionId, Map<String, Object> map) {
return Mono.fromSupplier(() -> this.delegate.apply(sessionId, map))
.onErrorResume(IllegalStateException.class,
(ex) -> this.sessionRepository.deleteById(sessionId).then(Mono.empty()));
}
}
}
自定义会话过期存储
由于 Redis 的性质,如果密钥未被访问,则无法保证何时会触发过期事件。 有关更多详细信息,请参阅有关密钥过期的 Redis 文档。
为了降低过期事件的不确定性,会话还会存储其预期的过期时间。
这可确保每个密钥在预期过期时都可以访问。
这RedisSessionExpirationStore
interface 定义了跟踪会话及其过期时间的常见作,并提供了清理过期会话的策略。
默认情况下,每个会话过期时间都跟踪到最接近的分钟。 这允许后台任务访问可能过期的会话,以确保以更具确定性的方式触发 Redis 过期事件。
例如:
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations:1439245080000 2100
然后,后台任务将使用这些映射显式请求每个会话过期密钥。 通过访问密钥而不是删除密钥,我们确保 Redis 仅在 TTL 过期时为我们删除密钥。
通过自定义会话过期存储,您可以根据需要更有效地管理会话过期。
为此,您应该提供一个RedisSessionExpirationStore
将由 Spring Session Data Redis 配置选取:
-
SessionConfig
import org.springframework.session.data.redis.SortedSetRedisSessionExpirationStore;
@Configuration
@EnableRedisIndexedHttpSession
public class SessionConfig {
@Bean
public RedisSessionExpirationStore redisSessionExpirationStore(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return new SortedSetRedisSessionExpirationStore(redisTemplate, RedisIndexedSessionRepository.DEFAULT_NAMESPACE);
}
}
In the code above, the SortedSetRedisSessionExpirationStore
implementation is being used, which uses a Sorted Set to store the session ids with their expiration time as the score.
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 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.
However, for your implementations you can choose the strategy that best fits.