Redis 配置
配置好應用後,您可能希望開始自定義一些設定。
-
我想使用 Spring Boot 屬性自定義 Redis 配置
-
我想獲得關於選擇
RedisSessionRepository
或RedisIndexedSessionRepository
的幫助。 -
我想指定不同的名稱空間。
-
自定義會話過期儲存
使用 JSON 序列化會話
預設情況下,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,因此我們建立了一個使用 Spring Security 的 Jackson 模組的自定義 ObjectMapper
。如果您不需要 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"
使用註解屬性
您可以透過在 @EnableRedisHttpSession
、@EnableRedisIndexedHttpSession
或 @EnableRedisWebSession
註解中設定 redisNamespace
屬性來指定 namespace
。
@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
還支援會話過期和刪除。
在 Redis Cluster 中使用 RedisIndexedSessionRepository 時,您必須注意它只訂閱叢集中一個隨機 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>
中。此 Map 需要經過對映過程才能轉換為 MapSession
物件,然後該物件在 RedisSession
中使用。
用於此目的的預設對映器稱為 RedisSessionMapper
。如果會話 Map 不包含構建會話所需的最小鍵(例如 creationTime
),此對映器將丟擲異常。缺少必需鍵的一種可能情況是,當會話鍵在儲存過程進行中時被併發刪除,通常是由於過期。發生這種情況是因為使用了 HSET 命令來設定鍵內的欄位,如果鍵不存在,此命令將建立它。
如果您想自定義對映過程,可以建立自己的 BiFunction<String, Map<String, Object>, MapSession>
實現並將其設定到會話儲存庫中。以下示例展示瞭如何將對映過程委託給預設對映器,但如果丟擲異常,則從 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
介面定義了跟蹤會話及其過期時間的通用操作,並提供了一種清理過期會話的策略。
預設情況下,每個會話過期都會跟蹤到最近一分鐘。這允許後臺任務訪問可能已過期的會話,以確保 Redis 過期事件以更確定的方式觸發。
例如
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations:1439245080000 2100
然後,後臺任務將使用這些對映顯式請求每個會話的過期鍵。透過訪問鍵而不是直接刪除它,我們可以確保 Redis 僅在 TTL 過期時刪除鍵。
透過自定義會話過期儲存,您可以根據需要更有效地管理會話過期。為此,您應該提供一個 RedisSessionExpirationStore
型別的 bean,它將被 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);
}
}
在上面的程式碼中,使用了 SortedSetRedisSessionExpirationStore
實現,它使用有序集合(Sorted Set)來儲存會話 ID,並將其過期時間作為分數。
我們不會顯式刪除鍵,因為在某些情況下可能存在競爭條件,導致錯誤地將未過期的鍵識別為已過期。除了使用分散式鎖(這會嚴重影響效能)之外,沒有辦法確保過期對映的一致性。透過簡單地訪問鍵,我們確保只有當該鍵的 TTL 過期時才刪除它。但是,對於您的實現,您可以選擇最適合您的策略。 |