一篇文章彻底理解Spring-Redis-Session机制
1.传统Session与Spring Session对比
传统容器session与应用绑定,保存在应用内存中,与容器形成一对一关系,如果多应用时无法实现session共享,比如session中保存用户信息,Spring Session通过巧妙的方式将session保存到一个公共的区域,支持可配置化方式,实现SessionRepository接口,可将session保存到Redis、Jdbc、Mongo等,图1表示两者的区别
(图1)
2. 先看下如何使用
2.1 在pom.xml加入依赖并安装redis服务
<!--spring-session-core 和 spring-data-redis集成包 用于分布式session管理-->
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
</dependency>
<!-- spring boot redis starter 用于自动装配-->
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
</dependency>
2.2 在SpringBoot启动类加上自动装配参数
public class RedisTestApplication{
public static void main(String[] args) {
SpringApplication.run(RedisTestApplication.class, args);
}
}
2.3 在application.yml上加上redis连接信息
spring:
redis:
database: 0
host: 192.168.1.123
port: 6379
password: *******
3. 开始揭密
3.1 对类的继承关系有个了解
SessionRepositoryRequestWrapper 与 SessionRepositoryResponseWrapper 是SessionRepositoryFilter的内部类, SessionRepositoryRequestWrapper 通过继承HttpServletRequestWrapper,采用装饰者模式,来扩展已有功能
HttpServletRequestWrapper中持有一个HttpServletRequest对象,然后实现HttpServletRequest接口的所有方法,所有方法实现中都是调用持有的HttpServletRequest对象的相应的方法。继承HttpServletRequestWrapper 可以对其重写。SessionRepositoryRequestWrapper继承HttpServletRequestWrapper,在构造方法中将原有的HttpServletRequest通过调用super完成对HttpServletRequestWrapper中持有的HttpServletRequest初始化赋值,然后重写和session相关的方法。这样就保证SessionRepositoryRequestWrapper的其他方法调用都是使用原有的HttpServletRequest的数据,只有session相关的是重写的逻辑。
Request相关类图 Response相关类图
3.2 开启自动装配
@EnableRedisHttpSession
3.3 开启RedisHttpSessionConfiguration
#导入RedisHttpSessionConfiguration这个 Spring Bean
public EnableRedisHttpSession
通过 RedisHttpSessionConfiguration 自动装配 RedisHttpSessionConfiguration
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration{
#定义sessionRepository,些Bean实现了SessionRepository,这个就是管理session 贮藏的接口,不同的贮藏方式会对应不同的方式
public RedisOperationsSessionRepository sessionRepository() {
#创建内置RedisTemplate,如我们业务操作Redis,可配置这个Bean
RedisTemplate redisTemplate = createRedisTemplate();
#调用构造法创建对象
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
#设置默认session失效时间
sessionRepository
.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
#设置redis刷新模式
sessionRepository.setRedisFlushMode(this.redisFlushMode);
#设置redis操作的数据库槽,默认为0
int database = resolveDatabase();
sessionRepository.setDatabase(database);
return sessionRepository;
}
public void setRedisConnectionFactory(
ObjectProvider springSessionRedisConnectionFactory,
ObjectProvider redisConnectionFactory) {
RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory
.getIfAvailable();
if (redisConnectionFactoryToUse == null) {
redisConnectionFactoryToUse = redisConnectionFactory.getObject();
}
this.redisConnectionFactory = redisConnectionFactoryToUse;
}
}
3.4 session 贮藏对应的接口层定义,分别对应session的增删改查,由不同贮藏方式自我实现
public interface SessionRepository<S extends Session> {
S createSession();
void save(S session);
S findById(String id);
void deleteById(String id);
}
3.5 通过redis方式实现的存储 RedisOperationsSessionRepository
public class RedisOperationsSessionRepository {
private final RedisOperations sessionRedisOperations;
public RedisOperationsSessionRepository(
RedisOperations sessionRedisOperations) {
//构造sessionTemplate
this.sessionRedisOperations = sessionRedisOperations;
//创建失效策略
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations,
this::getExpirationsKey, this::getSessionKey);
configureSessionChannels();
}
}
3.6 通过Servlet Api既可拿到redis中session,而不是原来的HttpSession,使用方式不变,这就是Spring-session 采用适配器模式来达到目地
HttpSession session = request.getSession(false);
3.7 其中的执行原理
3.8 分析 SessionRepositoryFilter,这是进行请求拦截改写HttpSession的入口,这是一个标准的Filter,通过责任链方式进行请求和响应处理,在处理请求前对httpServletRequest与httpServletResponse包装
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//保存sessionRepository
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//构建Request与Response的包装类
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
try {
//filter链式调用
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
//确保resonse在返回给client之前,提交session
wrappedRequest.commitSession();
}
}
request.getSession(false) 会调用 SessionRepositoryRequestWrapper. getSession()
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
S requestedSession = getRequestedSession();
if (requestedSession != null) {
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException(
"For debugging purposes only (not an error)"));
}
//调用sessionRepository创建session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
//设置最后访问时间
session.setLastAccessedTime(Instant.now());
//对redis进行包装
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
看下 session 结构图
看doc文档,Session接口是对所有Session相关类的一种定义, RedisSession内部 通过适配器模式重写HttpSession相关的方法,使用方法与原来一样,实际是操作RedisSession, RedisSession是MapSession是装饰器模式,MapSession是对Session的默认实现
/**
* Provides a way to identify a user in an agnostic way. This allows the session to be
* used by an HttpSession, WebSocket Session, or even non web related sessions.
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.0
*/
public interface Session {
...
}
了解下RedisSession的三种key, 创建一个RedisSession,会同时创建三个key-value
spring:session:sessions:sff1b336-dd96-4b33-11133d-df9424vgsdffe spring:session:sessions:expires:sff1b336-dd96-4b33-11133d-df9424vgsdffe spring:session:expirations:21233245080000
public static final String DEFAULT_NAMESPACE = "spring:session";
private String namespace = DEFAULT_NAMESPACE + ":";
String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
String getExpirationsKey(long expiration) {
return this.namespace + "expirations:" + expiration;
}
private String getExpiredKey(String sessionId) {
return getExpiredKeyPrefix() + sessionId;
}
private String getExpiredKeyPrefix() {
return this.namespace + "sessions:expires:";
}
在filter finally中调用 SessionRepositoryRequestWrapper提交commitSession
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,
this.response);
}
}
else {
S session = wrappedSession.getSession();
//清除当前request对象中session
clearRequestedSessionCache();
//调用sessionRepository来保存session
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!isRequestedSessionIdValid()
|| !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,
this.response, sessionId);
}
}
}
SessionRepositoryRequestWrapper.save()保存 session
//保存session到redis
public void save(RedisSession session) {
session.save();
if (session.isNew()) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}
//内部类,通过装饰模式包装MapSession,实现对MapSession操作
final class RedisSession implements Session {
private final MapSession cached;
private Instant originalLastAccessTime;
private Map delta = new HashMap<>();
private boolean isNew;
private String originalPrincipalName;
private String originalSessionId;
...
private void save() {
saveChangeSessionId();
saveDelta();
}
//开始保存session 中delta map数据到redis
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
//调用底层redis connection 保存hashmap值
getSessionBoundHashOperations(sessionId).putAll(this.delta);
...
}
}
转载自:https://juejin.cn/post/6963250840450629662