我的问题,当Redis服务器宕机时会发生什么? 在Redis恢复之前,Spring是否能够通过将Session存储在内存中来继续工作?有没有办法这样配置?
我在 AWS ElastiCache 上使用 Redis,在 DNS 上配置替换主节点之前,故障转移可能需要几分钟时间。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Component;
@Component("customSessionRepository")
@Primary
public class CustomFailoverToMapSessionRepository implements SessionRepository {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomFailoverToMapSessionRepository.class);
private GuavaBasedSessionRepository guavaBasedSessionRepository;
private SessionRepository sessionRepository;
public CustomFailoverToMapSessionRepository(SessionRepository sessionRepository, GuavaBasedSessionRepository guavaBasedSessionRepository) {
this.sessionRepository = sessionRepository;
this.guavaBasedSessionRepository = guavaBasedSessionRepository;
}
@Override
public Session createSession() {
Session session = null;
MapSession mapSession = guavaBasedSessionRepository.createSession();
try {
session = sessionRepository.createSession();
mapSession = toMapSession(session);
} catch (Exception e) {
LOGGER.warn("Unexpected exception when trying to create a session will create just an in memory session", e);
}
return session == null ? mapSession : session;
}
@Override
public void save(Session session) {
try {
if (!isOfMapSession(session)) {
sessionRepository.save(session);
}
} catch (Exception e) {
LOGGER.warn("Unexpected exception when trying to save a session with id {} will create just an in memory session", session.getId(), e);
}
guavaBasedSessionRepository.save(toMapSession(session));
}
@Override
public Session findById(String id) {
try {
return sessionRepository.findById(id);
} catch (Exception e) {
LOGGER.warn("Unexpected exception when trying to lookup a session with id {}", id, e);
return guavaBasedSessionRepository.findById(id);
}
}
@Override
public void deleteById(String id) {
try {
try {
guavaBasedSessionRepository.deleteById(id);
} catch (Exception e) {
//ignored
}
sessionRepository.deleteById(id);
} catch (Exception e) {
LOGGER.warn("Unexpected exception when trying to delete a session with id {}", id, e);
}
}
private boolean isOfMapSession(Session session) {
return session instanceof MapSession;
}
private MapSession toMapSession(Session session) {
final MapSession mapSession = guavaBasedSessionRepository.createSession();
if (session != null) {
mapSession.setId(session.getId());
mapSession.setCreationTime(session.getCreationTime());
mapSession.setLastAccessedTime(session.getLastAccessedTime());
mapSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
session.getAttributeNames()
.forEach(attributeName -> mapSession.setAttribute(attributeName, session.getAttribute(attributeName)));
}
return mapSession;
}
使用Guava实现内存缓存会话存储库
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Component("guavaBasedSessionRepository")
public class GuavaBasedSessionRepository implements SessionRepository<MapSession> {
private Cache<String, Session> sessionCache;
@Value("${session.local.guava.cache.maximum.size}")
private int maximumCacheSize;
@Value("${redis.session.keys.timeout}")
private long sessionTimeout;
@PostConstruct
void init(){
sessionCache = CacheBuilder
.newBuilder()
.maximumSize(maximumCacheSize)
.expireAfterWrite(sessionTimeout, TimeUnit.MINUTES)
.build();
}
@Override
public void save(MapSession session) {
if (!session.getId().equals(session.getOriginalId())) {
this.sessionCache.invalidate(session.getOriginalId());
}
this.sessionCache.put(session.getId(), new MapSession(session));
}
@Override
public MapSession findById(String id) {
Session saved = null;
try {
saved = this.sessionCache.getIfPresent(id);
} catch (Exception e){
//ignored
}
if (saved == null) {
return null;
}
if (saved.isExpired()) {
deleteById(saved.getId());
return null;
}
return new MapSession(saved);
}
@Override
public void deleteById(String id) {
this.sessionCache.invalidate(id);
}
@Override
public MapSession createSession() {
MapSession result = new MapSession();
result.setMaxInactiveInterval(Duration.ofSeconds(sessionTimeout));
return result;
}
配置 Spring 使用自定义 SessionRepository
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.session.Session;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieHttpSessionIdResolver;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.SessionRepositoryFilter;
import javax.annotation.PostConstruct;
@EnableRedisHttpSession
@Configuration
public class CustomSessionConfig {
private CookieHttpSessionIdResolver defaultHttpSessionIdResolver = new CookieHttpSessionIdResolver();
@Autowired
private CookieSerializer cookieSerializer;
@PostConstruct
public void init(){
this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}
@Bean
@Primary
public <S extends Session> SessionRepositoryFilter<? extends Session> sessionRepositoryFilter(CustomFailoverToMapSessionRepository customSessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(customSessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.defaultHttpSessionIdResolver);
return sessionRepositoryFilter;
}
CacheErrorHandler
(javadoc) 的实现。 您可以通过提供一个
Configuration
实例来实现此目的,该实例实现
CachingConfigurer
,并重写
errorHandler()
方法。例如:
@Configuration
@Ena1bleCaching
public class MyApp extends SpringBootServletInitializer implements CachingConfigurer {
@Override
public CacheErrorHandler errorHandler() {
return MyAppCacheErrorHandler();
}
}
我不清楚您将如何提供不间断的服务 - 如果不复制故障转移缓存中的当前会话,这似乎是不可能的。
如果您使用 ElasticCache,是否可以让 AWS 为您处理复制设置,以便在一个节点发生故障时,另一个节点可以接管?