Redis 配置

使用 JSON 序列化 Session

默认情况下,Spring Session 使用 Java 序列化来序列化会话属性。 有时可能会出现问题,尤其是当您有多个应用程序使用同一个 Redis 实例但具有同一类的不同版本时。 您可以提供RedisSerializerbean 来自定义如何将会话序列化为 Redis。 Spring Data Redis 提供了GenericJackson2JsonRedisSerializer它使用 Jackson 的ObjectMapper.spring-doc.cadn.net.cn

配置 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 模块,则可以将应用程序的ObjectMapperbean 并按如下方式使用它:spring-doc.cadn.net.cn

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

指定不同的命名空间

多个应用程序使用同一个 Redis 实例的情况并不少见。 因此,Spring Session 使用namespace(默认为spring:session) 以将会话数据分开。spring-doc.cadn.net.cn

使用 Spring Boot 属性

您可以通过设置spring.session.redis.namespace财产。spring-doc.cadn.net.cn

application.properties
spring.session.redis.namespace=spring:session:myapplication
application.yml
spring:
  session:
    redis:
      namespace: "spring:session:myapplication"

使用 Annotation 的属性

您可以指定namespace通过设置redisNamespace属性在@EnableRedisHttpSession,@EnableRedisIndexedHttpSession@EnableRedisWebSession附注:spring-doc.cadn.net.cn

@EnableRedisHttpSession
@Configuration
@EnableRedisHttpSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
    // ...
}
@EnableRedisIndexedHttpSession
@Configuration
@EnableRedisIndexedHttpSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
    // ...
}
@EnableRedisWebSession
@Configuration
@EnableRedisWebSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
    // ...
}

选择之间RedisSessionRepositoryRedisIndexedSessionRepository

使用 Spring Session Redis 时,你可能必须在RedisSessionRepositoryRedisIndexedSessionRepository. 两者都是SessionRepository在 Redis 中存储会话数据的接口。 但是,它们在处理会话索引和查询的方式上有所不同。spring-doc.cadn.net.cn

  • RedisSessionRepository:RedisSessionRepository是一种基本实现,可将会话数据存储在 Redis 中,而无需任何其他索引。 它使用简单的键值结构来存储会话属性。 每个会话都分配有一个唯一的会话 ID,会话数据存储在与该 ID 关联的 Redis 密钥下。 当需要检索会话时,存储库使用会话 ID 查询 Redis 以获取关联的会话数据。 由于没有索引,因此根据会话 ID 以外的属性或条件查询会话可能效率低下。spring-doc.cadn.net.cn

  • RedisIndexedSessionRepository:RedisIndexedSessionRepository是一种扩展实现,可为 Redis 中存储的会话提供索引功能。 它在 Redis 中引入了额外的数据结构,以根据属性或条件高效地查询会话。 除了RedisSessionRepository,它会维护其他索引以实现快速查找。 例如,它可能会根据会话属性(如用户 ID 或上次访问时间)创建索引。 这些索引允许根据特定条件高效查询会话,从而提高性能并启用高级会话管理功能。 除此之外,RedisIndexedSessionRepository还支持会话过期和删除。spring-doc.cadn.net.cn

使用RedisIndexedSessionRepository使用 Redis 集群时,您必须注意,它仅订阅来自集群中一个随机 Redis 节点的事件,如果事件发生在不同的节点中,这可能会导致某些会话索引无法清理。

配置RedisSessionRepository

使用 Spring Boot 属性

如果您使用的是 Spring Boot,则RedisSessionRepository是默认实现。 但是,如果您想明确说明这一点,则可以在应用程序中设置以下属性:spring-doc.cadn.net.cn

application.properties
spring.session.redis.repository-type=default
application.yml
spring:
  session:
    redis:
      repository-type: default

使用注释

您可以配置RedisSessionRepository通过使用@EnableRedisHttpSession注解:spring-doc.cadn.net.cn

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

配置RedisIndexedSessionRepository

使用 Spring Boot 属性

您可以配置RedisIndexedSessionRepository通过在应用程序中设置以下属性:spring-doc.cadn.net.cn

application.properties
spring.session.redis.repository-type=indexed
application.yml
spring:
  session:
    redis:
      repository-type: indexed

使用注释

您可以配置RedisIndexedSessionRepository通过使用@EnableRedisIndexedHttpSession注解:spring-doc.cadn.net.cn

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

侦听会话事件

通常,对会话事件做出反应是有价值的,例如,您可能希望根据会话生命周期进行某种处理。 为了能够做到这一点,您必须使用索引存储库。 如果您不知道已编入索引的存储库和默认存储库之间的区别,可以转到此部分spring-doc.cadn.net.cn

配置索引存储库后,您现在可以开始侦听SessionCreatedEvent,SessionDeletedEvent,SessionDestroyedEventSessionExpiredEvent事件。 在 Spring 中有几种方法可以监听应用程序事件,我们将使用@EventListener注解。spring-doc.cadn.net.cn

@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
    }

}

查找特定用户的所有会话

通过检索特定用户的所有会话,您可以跨设备或浏览器跟踪用户的活动会话。 例如,您可以使用此信息进行会话管理,例如允许用户使特定会话无效或注销,或者根据用户的会话活动执行作。spring-doc.cadn.net.cn

为此,首先您必须使用索引存储库,然后您可以注入FindByIndexNameSessionRepository接口,如下所示:spring-doc.cadn.net.cn

@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方法删除用户的特定会话。spring-doc.cadn.net.cn

配置 Redis 会话映射器

Spring Session Redis 从 Redis 中检索会话信息并将其存储在Map<String, Object>. 此映射需要经过映射过程才能转换为MapSessionobject,然后在RedisSession.spring-doc.cadn.net.cn

用于此目的的默认映射器称为RedisSessionMapper. 如果会话映射不包含构建会话所需的最小键,例如creationTime,此 mapper 将引发异常。 缺少所需密钥的一种可能情况是,在保存过程正在进行时,会话密钥通常由于过期而被同时删除。 发生这种情况是因为 HSET 命令用于设置键中的字段,如果键不存在,此命令将创建它。spring-doc.cadn.net.cn

如果要自定义映射过程,可以创建BiFunction<String, Map<String, Object>, MapSession>并将其设置到 Session 存储库中。 以下示例显示了如何将映射过程委托给默认映射器,但如果引发异常,则会从 Redis 中删除该会话:spring-doc.cadn.net.cn

@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 文档。spring-doc.cadn.net.cn

为了降低过期事件的不确定性,会话还会存储其预期的过期时间。 这可确保每个密钥在预期过期时都可以访问。 这RedisSessionExpirationStoreinterface 定义了跟踪会话及其过期时间的常见作,并提供了清理过期会话的策略。spring-doc.cadn.net.cn

默认情况下,每个会话过期时间都跟踪到最接近的分钟。 这允许后台任务访问可能过期的会话,以确保以更具确定性的方式触发 Redis 过期事件。spring-doc.cadn.net.cn

SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations:1439245080000 2100

然后,后台任务将使用这些映射显式请求每个会话过期密钥。 通过访问密钥而不是删除密钥,我们确保 Redis 仅在 TTL 过期时为我们删除密钥。spring-doc.cadn.net.cn

通过自定义会话过期存储,您可以根据需要更有效地管理会话过期。 为此,您应该提供一个RedisSessionExpirationStore将由 Spring Session Data Redis 配置选取:spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn