本片文章适合对spring-session的工作原理有所理解的同学,如果还没有理解spring-session的核心原理,可以参考spring-session 原理及源码解析
1 spring-session-data-redis工作原理
在spring-session过滤session的基础上,spring-session-data-redis做了redis的实现,使我们可以通过redis来集中管理session。由于redis的高性能、易管理、持久化等特性,spring-session-data-redis经常被作为spring-session仓库的主要选择。sessionRepository是spring-session框架中管理session的接口,spring-session-data-redis的RedisOperationsSessionRepository实现了这个接口,自定义了session管理细节。我们先来看RedisOperationsSessionRepository的UML图:
它实现了session检索仓库FindByIndexNameSessionRepository,因此具备检索功能,同时实现了MessageListener,可以监听redis发布/订阅的消息。在其内部自定义了RedisSession,RedisSession实现了ExpiringSession,因此具备超时功能。
session的管理是通过三个key来管理session,便于区分,下问以简称代替
key | 简称 | 类型 | value | 超时时间 |
spring:session:sessions:sessionId | sessionKey | Hash | session的属性 | 超时+5分钟 |
spring:session:sessions:expires:sessionId | 超时sessionKey | String | 超时 | |
spring:session:expirations:时间戳(最后访问时间+超时时间) | 时间戳key | Set | expires:sessionId | 超时+5分钟 |
1 session的管理
1.1 创建session
public RedisSession createSession() {
//新建RedisSession,并设置超时时间
RedisSession redisSession = new RedisSession();
if (this.defaultMaxInactiveInterval != null) {
redisSession.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);
}
return redisSession;
}
1.2 保存session
public void save(RedisSession session) {
// 保存sesion
session.saveDelta();
//如果是新创建的session,发布一个session创建消息
if (session.isNew()) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}
saveDelta的流程图如下:
1.3 清除session
public void delete(String sessionId) {
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
cleanupPrincipalIndex(session);
//触发onDelete,移除时间戳Key中的sessionId
this.expirationPolicy.onDelete(session);
//移除超时sessionkey
String expireKey = getExpiredKey(session.getId());
this.sessionRedisOperations.delete(expireKey);
//清零超时时间,重新保存session
session.setMaxInactiveIntervalInSeconds(0);
save(session);
}
1.4 定时清除过期session
这里使用spring定时器,每分钟调用一次清理过期session操作,这个定时策略可通过自定义属性spring.session.cleanup.cron.expression来设置。
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
/**
* 清除过期的session
*/
public void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);
if (logger.isDebugEnabled()) {
logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
}
String expirationKey = getExpirationKey(prevMin);
Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
//删除当前分钟的时间戳Key
this.redis.delete(expirationKey);
for (Object session : sessionsToExpire) {
//移除过期的sessionKey
String sessionKey = getSessionKey((String) session);
touch(sessionKey);
}
}
1.5 获取session
public RedisSession getSession(String id) {
return getSession(id, false);
}
private RedisSession getSession(String id, boolean allowExpired) {
//从sessionKey中获取所有的Hash属性
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
//不存在则返回null
if (entries.isEmpty()) {
return null;
}
//重新封装进一个MapSession
MapSession loaded = loadSession(id, entries);
//确保获取未过期的session
if (!allowExpired && loaded.isExpired()) {
return null;
}
//从新封装一个RedisSession
RedisSession result = new RedisSession(loaded);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
1.6 获取某个索引绑定的所有session
public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName,
String indexValue) {
// 如果索引名称不是指定名称,则返回空map
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
// 用indexValue组装principalKey
String principalKey = getPrincipalKey(indexValue);
// 获取principalKey的所有sessionId集合
Set<Object> sessionIds = this.sessionRedisOperations.boundSetOps(principalKey)
.members();
// 封装进一个Map<String, RedisSession>并返回
Map<String, RedisSession> sessions = new HashMap<String, RedisSession>(
sessionIds.size());
for (Object id : sessionIds) {
RedisSession session = getSession((String) id);
if (session != null) {
sessions.put(session.getId(), session);
}
}
return sessions;
}
1.7 移除索引绑定的session
private void cleanupPrincipalIndex(RedisSession session) {
String sessionId = session.getId();
String principal = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(session);
if (principal != null) {
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal))
.remove(sessionId);
}
}
2 事件的支持
spring-session定义了AbstractSessionEvent来实现事件的支持,这个抽象类继承了ApplicationEvent,这里使用了spring的事件支持,spring-session又定义了SessionCreatedEvent,SessionDeletedEvent,SessionDestroyedEvent,SessionExpiredEvent四个事件来支持session的创建、删除、销毁、过期。RedisOperationsSessionRepository实现了MessageListener,在监听到消息时做了如下操作:
public void onMessage(Message message, byte[] pattern) {
byte[] messageChannel = message.getChannel();
byte[] messageBody = message.getBody();
if (messageChannel == null || messageBody == null) {
return;
}
String channel = new String(messageChannel);
//如果通道以spring:session:event:created:开始,则反序列化message里的body,发布session创建事件
if (channel.startsWith(getSessionCreatedChannelPrefix())) {
// TODO: is this thread safe?
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer
.deserialize(message.getBody());
handleCreated(loaded, channel);
return;
}
String body = new String(messageBody);
// 如果body不以spring:session:sessions:expires:开始,则返回。
if (!body.startsWith(getExpiredKeyPrefix())) {
return;
}
//如果通道以:del结尾,则是删除通道
boolean isDeleted = channel.endsWith(":del");
//如果是删除通道或者以:expired结尾
if (isDeleted || channel.endsWith(":expired")) {
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
RedisSession session = getSession(sessionId, true);
if (session == null) {
logger.warn("Unable to publish SessionDestroyedEvent for session "
+ sessionId);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
//移除索引绑定session
cleanupPrincipalIndex(session);
if (isDeleted) {
//发布删除事件
handleDeleted(session);
}
else {
//发布过期事件
handleExpired(session);
}
}
}
因此,spring-session-data-redis是通过redis的发布订阅机制,触发spring的事件发布机制,从而实现支持session事件的功能。如果我们想使用这个功能,只需要实现spring的事件监听接口ApplicationListener,重写onApplicationEvent即可。
spring-session-data-redis 1.3.x的整合从注解@EnableRedisHttpSession入手,EnableRedisHttpSession引入了RedisHttpSessionConfiguration类,主要配置在RedisHttpSessionConfiguration中。我们来看看RedisHttpSessionConfiguration都做了什么操作。
2 可自定义的部分
1 RedisHttpSessionConfiguration
1.1 自定义sessionRedisTemplate
通过@Bean注解定义注入仅供spring-session操作的RedisTemplate(sessionRedisTemplate),这里可以通过自定义bean(springSessionDefaultRedisSerializer)来指定默认序列化方式,使得我们在引入spring-session框架时,不会影响之前的RedisTemplate属性。如果我们需要额外的修改,可以通过bean标签在xml中自定义sessionRedisTemplate来覆盖这个sessionRedisTemplate。
@Autowired(required = false)
@Qualifier("springSessionDefaultRedisSerializer")
public void setDefaultRedisSerializer(
RedisSerializer<Object> defaultRedisSerializer) {
this.defaultRedisSerializer = defaultRedisSerializer;
}
@Bean
public RedisTemplate<Object, Object> sessionRedisTemplate(
RedisConnectionFactory connectionFactory) {
//创建一个新的RedisTemplate,定义序列化为StringRedisSerializer,
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
//这里我们可以指定默认序列化
if (this.defaultRedisSerializer != null) {
template.setDefaultSerializer(this.defaultRedisSerializer);
}
template.setConnectionFactory(connectionFactory);
return template;
}
1.2 创建session仓库RedisOperationsSessionRepository
@Bean
public RedisOperationsSessionRepository sessionRepository(
@Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate,
ApplicationEventPublisher applicationEventPublisher) {
//创建一个RedisOperationsSessionRepository实例
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
sessionRedisTemplate);
//添加指定的ApplicationEventPublisher
sessionRepository.setApplicationEventPublisher(applicationEventPublisher);
//指定session超时时间
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
//指定默认序列化方式
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
//指定redis命名空间
String redisNamespace = getRedisNamespace();
if (StringUtils.hasText(redisNamespace)) {
sessionRepository.setRedisKeyNamespace(redisNamespace);
}
//指定flush模式
sessionRepository.setRedisFlushMode(this.redisFlushMode);
return sessionRepository;
}
1.3 指定ConfigureRedisAction
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
@Autowired(required = false)
public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) {
this.configureRedisAction = configureRedisAction;
}
@Bean
public InitializingBean enableRedisKeyspaceNotificationsInitializer(
RedisConnectionFactory connectionFactory) {
//通过RedisConnectionFactory和ConfigureRedisAction创建EnableRedisKeyspaceNotificationsInitializer,我们可以指定ConfigureRedisAction
return new EnableRedisKeyspaceNotificationsInitializer(connectionFactory,
this.configureRedisAction);
}
1.4 自定义springSessionRedisTaskExecutor(bean),指定redisTaskExecutor线程池
@Autowired(required = false)
@Qualifier("springSessionRedisTaskExecutor")
public void setRedisTaskExecutor(Executor redisTaskExecutor) {
this.redisTaskExecutor = redisTaskExecutor;
}
1.5 自定义springSessionRedisSubscriptionExecutor(bean),指定redisSubscriptionExecutor线程池
@Autowired(required = false)
@Qualifier("springSessionRedisSubscriptionExecutor")
public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) {
this.redisSubscriptionExecutor = redisSubscriptionExecutor;
}
2 EnableRedisHttpSession
EnableRedisHttpSession可以自定义三个属性:
属性名称 | 类型 | 描述 | 默认 |
maxInactiveIntervalInSeconds | int 单位(秒) | session超时时间 | 1800 |
redisNamespace | String | redis命名空间 | "" |
redisFlushMode | RedisFlushMode | redis刷新模式 | RedisFlushMode.ON_SAVE |