1.前置知识: Keyspace Notifications (redis 键空间通知)
http://www.redis.cn/topics/notifications.html
https://redis.io/topics/notifications
https://www.jianshu.com/p/2c3f253fb4c5
2.应用示例
2.1 pom.xml
<!-- <version>2.2.6.RELEASE</version> --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2.2 application.yml
spring: redis: host: 192.168.0.156 port: 6379 session: store-type: redis timeout: 30m redis: # 表示不支持 键空间事件; 默认值是 notify_keyspace_events 表示支持 # 这里有个问题就是, 如果一开始启用了该功能, 后期想关闭该功能, 仅把此处设置为 none 是不行的, 必须重启 redis, 再讲此处设置为 none. # 再研究下, 看是 bug 提个 issue, 还是说 还有其他方案. configure-action: none cleanup-cron: 0 * * * * * # 清理过期 session 的定时任务 namespace: spring:session # 前缀 server: port: 8081 servlet: session: timeout: 30m cookie: http-only: true # domain: zy.com path: / # secure: true name: authId
2.3 模拟登录登出的controller
package com.zy.controller; import com.zy.vo.TbUser; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** * 分别模拟登录和登出 */ @RestController @RequestMapping("/user/") public class UserController { public static final String USER_ATTRIBUTE_NAME = "user_session"; @RequestMapping("login") public ResponseEntity<String> login(@RequestBody TbUser user, HttpServletRequest request) { request.getSession().setAttribute(USER_ATTRIBUTE_NAME, user); return ResponseEntity.ok().body("successfully to login.\n sessionId is: " + request.getSession().getId()); } @RequestMapping("logout") public ResponseEntity<String> logout(HttpServletRequest request) { request.getSession(false).invalidate(); return ResponseEntity.ok().body("successfully to logout..\n sessionId is: " + request.getSession().getId()); } }
2.4 监听Session删除事件的监听器
package com.zy.config; import com.zy.controller.UserController; import org.springframework.context.ApplicationListener; import org.springframework.session.events.SessionDeletedEvent; import org.springframework.stereotype.Component; @Component public class SessionDeleteEventListener implements ApplicationListener<SessionDeletedEvent> { @Override public void onApplicationEvent(SessionDeletedEvent event) { System.out.println("--------------------------------"); Object user = event.getSession().getAttribute(UserController.USER_ATTRIBUTE_NAME); System.out.println("SessionDeletedEvent, user is: " + user); System.out.println("--------------------------------"); } }
2.5 MockMvc 进行mock测试
package com.zy; import com.alibaba.fastjson.JSON; import com.zy.vo.TbUser; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import javax.servlet.http.Cookie; @SpringBootTest @AutoConfigureMockMvc @RunWith(SpringRunner.class) public class TestUserController { @Autowired private MockMvc mockMvc; @Test public void testLoginAndLogout() throws Exception { TbUser user = new TbUser(); user.setUsername("tom"); user.setPassword("123456"); ResultActions actions = this.mockMvc.perform(MockMvcRequestBuilders.post("/user/login") .content(JSON.toJSONString(user)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()); String sessionId = actions.andReturn().getResponse().getCookie("authId").getValue(); ResultActions resultActions = this.mockMvc.perform(MockMvcRequestBuilders.post("/user/logout") .cookie(new Cookie("authId", sessionId))) .andExpect(MockMvcResultMatchers.status().isOk()); } }
3.源码分析
3.1 框架初始化阶段
3.1.1 加载 yml 中的配置文件
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties { } @ConfigurationProperties(prefix = "spring.session") public class SessionProperties { }
3.1.2 加载相关自动装配的类
核心类1 SessionAutoConfiguration // 构建 DefaultCookieSerializer SessionAutoConfiguration.ServletSessionConfiguration#cookieSerializer
核心类2 RedisSessionConfiguration // 构建 ConfigureRedisAction (即redis键空间通知相关行为) RedisSessionConfiguration#configureRedisAction >> NOTIFY_KEYSPACE_EVENTS >> NONE // 构建 RedisIndexedSessionRepository 前的属性设置 RedisSessionConfiguration.SpringBootRedisHttpSessionConfiguration#customize >> setMaxInactiveIntervalInSeconds >> setRedisNamespace >> setFlushMode >> setSaveMode >> setCleanupCron
核心类3 RedisHttpSessionConfiguration // 构建 RedisIndexedSessionRepository RedisHttpSessionConfiguration#sessionRepository 在其构造器中构建了 RedisSessionExpirationPolicy (核心类), 其核心方法: >> onDelete >> onExpirationUpdated >> cleanExpiredSessions // 构建 RedisMessageListenerContainer RedisHttpSessionConfiguration#redisMessageListenerContainer 在该对象中 addMessageListener (核心方法), 主要添加了: >> SessionDeletedTopic >> SessionExpiredTopic >> SessionCreatedTopic. 这些 Topic 最终被 RedisIndexedSessionRepository 实现了其 onMessage 方法, 监听 Session 的创建, 删除, 过期事件. 该onMessage 方法 在 RedisMessageListenerContainer#executeListener 中调用时触发. 该类中同时有一个内部SubscriptionTask, 该线程主要是订阅redis事件. // 构建 EnableRedisKeyspaceNotificationsInitializer // 这里如果 redis 不支持 键空间通知 功能, 启动时便会抛异常 RedisHttpSessionConfiguration#enableRedisKeyspaceNotifications >> 如果上述步骤(核心类1)中 ConfigureRedisAction 是 NONE, 则不向 redis 发送指令, 不再支持"键空间通知"; >> 如果 ConfigureRedisAction 是 NOTIFY_KEYSPACE_EVENTS,则向redis发送指令, 支持"键空间通知"; // 构建清理过期的 sessions 的调度任务 (定时任务, cron 表达式) RedisHttpSessionConfiguration.SessionCleanupConfiguration
核心类4 SpringHttpSessionConfiguration // 核心点: 构建 SessionEventHttpSessionListenerAdapter SpringHttpSessionConfiguration#sessionEventHttpSessionListenerAdapter // 核心点: 构建 SessionRepositoryFilter SpringHttpSessionConfiguration#springSessionRepositoryFilter 其默认优先级是: Integer.MIN_VALUE + 50; 值越小, 优先级越高.
核心类5 SessionRepositoryFilterConfiguration // 将 SessionRepositoryFilter 注册到 filter 链中 SessionRepositoryFilterConfiguration#sessionRepositoryFilterRegistration 默认的拦截路径是: /*
3.2 运行时拦截阶段
5.一些问题
5.1 关于spring-boot整合方式的问题
spring-boot2里:
个人不太建议 使用@EnableRedisHttpSession 注解了. 如果用, 则RedisSessionConfiguration 配置类将不被加载.
此时若是配置 spring.session.redis.configure-action=none (即不启用键空间通知事件), 将不生效.
5.2 启用键空间事件无法关闭的bug
#spring-boot 版本2.2.6.RELEASE 如果spring-boot中一开始支持"键空间通知" 其实是通过RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer#afterPropertiesSet中发送 config set命令来使得redis支持的. 如果后期想关闭该功能. 仅仅配置spring.session.redis.configure-action=none是不起作用的, 需要重启 redis服务器. (也许还有其他方案, 也许可以提个 issue).
5.3 spring-session在redis集群下监听expired事件失败
spring-session的expired事件有时监听会丢失,spring-session不支持redis集群场景。 其原因在于: spring-session默认会随机订阅redis集群中所有主备节点中一台, 而创建带ttl参数的session连接只会hash到所有主节点中一台。 只有订阅和session创建连接同时连接到一台redis节点才能监听到这个ttl session产生的expired事件。 解决方案: 自行对所有redis集群主备节点进行expired事件订阅。
https://github.com/spring-projects/spring-session/issues/478
参考资源
https://www.cnblogs.com/lxyit/archive/2004/01/13/9720159.html
https://www.cnblogs.com/tinywan/p/5903988.html
https://blog.csdn.net/gqtcgq/article/details/50808729
原文:https://www.cnblogs.com/psy-code/p/13763629.html