spring boot + redis 实现session共享分析

思维导图:

图片.png

一、为什么需要session共享

HttpSession是由servelet容器进行管理的。而我们常用的应用容器有 Tomcat/Jetty等, 这些容器的HttpSession都是存放在对应的应用容器的内存中,在分布式集群的环境下,通常我们使用Nginx或者LVS、Zuul等进行反向代理和负载均衡,因此用户请求是由一组提供相同服务的应用来进行处理,而用户最终请求到的服务由Nginx和LVS、Zuul进行确定。

例如:我们现在有2相同的服务,服务A和服务B,通过Nginx进行反向代理和负载均衡,用户请求,登录时由服务A进行处理,而修改用户资料有服务B进行处理。当前HttpSession是存放在服务A的内存中,而进行修改资料的时候由服务B进行处理,这时候服务B是不可能获取到服务A的HttpSession。因此请求修改用户资料的请求会失败。

那么问题就来了,我们怎样保证多个相同的应用共享同一份session数据?对于这种问题Spring为我们提供了Spring Session进行管理我们的HttpSession。项目地址:http://projects.spring.io/spring-session/

二、Spring Session搭建

1.添加Spring session的包,而Spring session 是将HttpSession存放在Redis中,因此需要添加Redis的包。我们这里是用了Spring boot进行配置Rdies。


  <groupId>com.test</groupId>
  <artifactId>SpringSession</artifactId>
  <version>0.0.1</version>
  <packaging>jar</packaging>
  <name>SpringSession</name>
  <url>http://maven.apache.org</url>
  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.0.RELEASE</version>
  </parent>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
        <!-- Spring boot 包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-redis</artifactId>
        </dependency>
        <!-- Spring session -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
  </dependencies>

2.使用@EnableRedisHttpSession注解进行配置启用使用Spring session。

@SpringBootApplication
@EnableRedisHttpSession
public class App 
{
    public static void main( String[] args )
    {
        SpringApplication.run(App.class, args);
    }
}

扩展知识:Spring Session提供了3种方式存储session的方式。分别对应3各种注
@EnableRedisHttpSession-存放在缓存redis
@EnableMongoHttpSession-存放在Nosql的MongoDB
@EnableJdbcHttpSession-存放数据库

3.配置我们的Redis链接,我们这里使用的是Spring Boot作为基础进行配置,因此我们只需要在YML或者Properties配置文件添加Redis的配置即可。

server:
  port: 8081

spring:
  application:
    name: manager
  profiles:
    active: dev
  redis: 
     database: 1
     host: 192.168.1.104
     password: 
     port: 6379

4.创建请求的控制器来进行确定我们是否启用Session 共享。

@RestController
public class SessionController {
    
    @GetMapping("/setUrl")
    public Map<String,Object> setUrl(HttpServletRequest request){
        request.getSession().setAttribute("url", request.getRequestURL());
        Map<String,Object> map = new HashMap<>();
        map.put("url", request.getRequestURL());
        return map;
    }

    @GetMapping("/getSession")
    public Map<String,Object> getSession(HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();
        map.put("sessionId", request.getSession().getId());
        map.put("url", request.getSession().getAttribute("url"));
        return map;
    }
}

5.将当前的工程拷贝一份.

修改YML或者Properties配置文件中的端口。原工程的端口为:8080,我们拷贝的工程修改成为:8081

(1)执行请求:http://localhost:8080/setUrl
     界面显示:{"url":"http://localhost:8080/setUrl"}
(2)执行请求:http://localhost:8081/getSession,查看是否显示之前设置在Session中的属性
     界面显示:{"sessionId":"e8c50c54-9aa7-4c34-bcea-a648242dfd0b","url":"http://localhost:8080/setUrl"}
(3)执行请求:http://localhost:8080/getSession
      界面显示:{"sessionId":"e8c50c54-9aa7-4c34-bcea-a648242dfd0b","url":"http://localhost:8080/setUrl"}

通过上面请求显示的结果我们可以看出使用的是同一个Seesion,我们也可以查看下存在Redis中的Session。我这里使用RDM进行查看,我们还可以查看Session的属性。从图可以看出我们存进入的url属性。

Snip20170413_2.png

二、Spring Session源码分析

我们从启动Spring Session的配置注解@EnableRedisHttpSession开始。
1.我们可以通过@EnableRedisHttpSession可以知道,Spring Session是通过RedisHttpSessionConfiguration类进行配置的。

@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
    int maxInactiveIntervalInSeconds() default 1800;

2.我们在RedisHttpSessionConfiguration类种的注释可以知道,该类是用于创建一个过滤SessionRepositoryFilter。

/**
 * Exposes the {@link SessionRepositoryFilter} as a bean named
 * "springSessionRepositoryFilter". In order to use this a single
 * {@link RedisConnectionFactory} must be exposed as a Bean.
 *
 * @author Rob Winch
 * @since 1.0
 *
 * @see EnableRedisHttpSession
 */
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements ImportAware {

3.探究下SessionRepositoryFilter类是在哪里创建\创建过程\作用。
(1)哪里创建:
通过搜索RedisHttpSessionConfiguration发现SessionRepositoryFilter的创建不是在RedisHttpSessionConfiguration,而是在父类SpringHttpSessionConfiguration中创建。

@Bean
    public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(
            SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(
                sessionRepository);
        sessionRepositoryFilter.setServletContext(this.servletContext);
        if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
            sessionRepositoryFilter.setHttpSessionStrategy(
                    (MultiHttpSessionStrategy) this.httpSessionStrategy);
        }
        else {
            sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
        }
        return sessionRepositoryFilter;
    }

为什么会在父类种进行创建呢?因为Spring Session 是提供多种存储Session的策略,因此会把创建SessionRepositoryFilter的方法放在SpringHttpSessionConfiguration中,而把每种策略特有的链接和操作放在了子类当中。

(2)SessionRepositoryFilter创建过程:

  • SessionRepositoryFilter的创建需要sessionRepository,而sessionRepository是一个接口,我可以通过查看接口发现该接口有一个扩展的子类接口FindByIndexNameSessionRepository,从该接口的实现类种我们可以发现有3个对应实现类分别为:
  • RedisOperationsSessionRepository
  • MongoOperationsSessionRepository
  • JdbcOperationsSessionRepository
  • 我们使用的是Redis的Session共享,因此这里使用到的实现类为RedisOperationsSessionRepository,而该类的初始化是在RedisHttpSessionConfiguration中。在初始化的时候需要一个sessionRedisTemplate参数,而该参数也在RedisHttpSessionConfiguration中进行初始化
    @Bean
    public RedisTemplate<Object, Object> sessionRedisTemplate(
            RedisConnectionFactory connectionFactory) {
        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;
    }
@Bean
    public RedisOperationsSessionRepository sessionRepository(
            @Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate,
            ApplicationEventPublisher applicationEventPublisher) {
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
                sessionRedisTemplate);
        sessionRepository.setApplicationEventPublisher(applicationEventPublisher);
        sessionRepository
                .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
        if (this.defaultRedisSerializer != null) {
            sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
        }

        String redisNamespace = getRedisNamespace();
        if (StringUtils.hasText(redisNamespace)) {
            sessionRepository.setRedisKeyNamespace(redisNamespace);
        }

        sessionRepository.setRedisFlushMode(this.redisFlushMode);
        return sessionRepository;
    }

我们在创建RedisOperationsSessionRepository的时候需要一个applicationEventPublisher的参数,而applicationEventPublisher主要用于发布事件。当创建session:handleCreated();删除session:handleDeleted();session过期:handleExpired();时都会发布事件,而事件的处理是由SessionEventHttpSessionListenerAdapter进行接受后分配到HttpSessionMutexListener进行实际处理。对Session增加SESSION_MUTEX_ATTRIBUTE属性,而该属性主要用于保证Session在其生命周期中都是唯一,并且使当前的Session是线程安全的。

这里我们可以总结下:
Redis确保链接的情况下。
1.创建sessionRedisTemplate
2.创建RedisOperationsSessionRepository
3.创建SessionRepositoryFilter

(3)SessionRepositoryFilter的作用:
SessionRepositoryFilter的主要作用接管Seession的管理。我们可以从下面几个点知道为什么?

  • 我们从SessionRepositoryFilter注释可以看到,SessionRepositoryFilter放在访问的任何Filter之前。那我们怎么保证我们的SessionRepositoryFilter会在其他Filter之前执行呢?关键在于SessionRepositoryFilter是继承了OncePerRequestFilter,而OncePerRequestFilter是一个抽象的类,我们从注释中可以看到,该类是一个确保每一个请求前进行调用。正因为SessionRepositoryFilter继承了OncePerRequestFilter因此确保了SessionRepositoryFilter的优先级别高与其他Filter。
/**
 * Allows for easily ensuring that a request is only invoked once per request. This is a
 * simplified version of spring-web's OncePerRequestFilter and copied to reduce the foot
 * print required to use the session support.
 *
 * @author Rob Winch
 * @since 1.0
 */
abstract class OncePerRequestFilter implements Filter 
  • 我们会想那为什么需要将SessionRepositoryFilter放在所有的Filter之前呢?因为SessionRepositoryFilter使用SessionRepositoryRequestWrapper进行处理Session的管理。因此我们需要将SessionRepositoryFilter放在其他Filter之前。
@Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
                request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                wrappedRequest, response);

        HttpServletRequest strategyRequest = this.httpSessionStrategy
                .wrapRequest(wrappedRequest, wrappedResponse);
        HttpServletResponse strategyResponse = this.httpSessionStrategy
                .wrapResponse(wrappedRequest, wrappedResponse);

        try {
            filterChain.doFilter(strategyRequest, strategyResponse);
        }
        finally {
            wrappedRequest.commitSession();
        }
    }

4.我们研究下SessionRepositoryRequestWrapper是怎样接管Session?

(1)存储Session的过程

  • SessionRepositoryRequestWrapper继承了HttpServletRequestWrapper,而我们都知道HttpServletRequest对象的参数是不可改变的,使用HttpServletRequestWrapper尽管你不能改变不变对象本身,但你却可以通过使用装饰模式来改变HttpServletRequest状态。传递下去的HttpServletRequest就包含了我们增加的处理.
  • SessionRepositoryFilter每次调用完毕后都会调用commitSession()方法。 当前的Session不为空的情况下,保存当前的Session。
/**
         * Uses the HttpSessionStrategy to write the session id to the response and
         * persist the Session.
         */
        private void commitSession() {
            HttpSessionWrapper wrappedSession = getCurrentSession();
            if (wrappedSession == null) {
                if (isInvalidateClientSession()) {
                    SessionRepositoryFilter.this.httpSessionStrategy
                            .onInvalidateSession(this, this.response);
                }
            }
            else {
                S session = wrappedSession.getSession();
                //将Session存放到Redis中
SessionRepositoryFilter.this.sessionRepository.save(session);
                if (!isRequestedSessionIdValid()
                        || !session.getId().equals(getRequestedSessionId())) {
                    SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
                            this, this.response);
                }
            }
        }

当调用SessionRepositoryFilter.this.sessionRepository.save(session)完毕后,会判断当前的SessionId是否与请求的中的Cookie中SessionId一致,若不一致的情况下会调用onNewSession()方法,我们可以通过SpringHttpSessionConfiguration配置类的可以看到使用的是
CookieHttpSessionStrategy();
从CookieHttpSessionStrategy.onNewSession()方法可以看到是将SessionId写到Cookie中。

private CookieHttpSessionStrategy defaultHttpSessionStrategy = new CookieHttpSessionStrategy();
private HttpSessionStrategy httpSessionStrategy = this.defaultHttpSessionStrategy;

  @Bean
  public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(
          SessionRepository<S> sessionRepository) {
      SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(
              sessionRepository);
      sessionRepositoryFilter.setServletContext(this.servletContext);
      if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
          sessionRepositoryFilter.setHttpSessionStrategy(
                  (MultiHttpSessionStrategy) this.httpSessionStrategy);
      }
      else {
          sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
      }
      return sessionRepositoryFilter;
  }
public void onNewSession(Session session, HttpServletRequest request,
         HttpServletResponse response) {
     Set<String> sessionIdsWritten = getSessionIdsWritten(request);
     if (sessionIdsWritten.contains(session.getId())) {
         return;
     }
     sessionIdsWritten.add(session.getId());

     Map<String, String> sessionIds = getSessionIds(request);
     String sessionAlias = getCurrentSessionAlias(request);
     sessionIds.put(sessionAlias, session.getId());

     String cookieValue = createSessionCookieValue(sessionIds);
     this.cookieSerializer
             .writeCookieValue(new CookieValue(request, response, cookieValue));
 }

(2)获取Session的过程

  • 获取Session的过程,是调用SessionRepositoryRequestWrapper的 getSession(boolean create)方法。通过该方法我们可以分析到:
    第一:获取当前的Session,如果获取到直接返回。
    第二:如果获取不到当前的标记属性的Session,从Cookie中获取SessionId,在Redis中获取Session,判断是否获取到Session,若获取到Session将Session存放到当前请求中。若获取不到创建新的Session。

    public HttpSessionWrapper getSession(boolean create) {
          HttpSessionWrapper currentSession = getCurrentSession();
          if (currentSession != null) {
              return currentSession;
          }
          String requestedSessionId = getRequestedSessionId();
          if (requestedSessionId != null
                  && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
              S session = getSession(requestedSessionId);
              if (session != null) {
                  this.requestedSessionIdValid = true;
                  currentSession = new HttpSessionWrapper(session, 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)"));
          }
          S session = SessionRepositoryFilter.this.sessionRepository.createSession();
          session.setLastAccessedTime(System.currentTimeMillis());
          currentSession = new HttpSessionWrapper(session, getServletContext());
          setCurrentSession(currentSession);
          return currentSession;
      }
    

总结:

我们根据源码的分析可以知道:
1.Spring Session 是通过SessionRepositoryFilter过滤器进行拦截,然后通过SessionRepositoryRequestWrapper继承HttpServletRequestWrapper进行管理Session。

2.Spring Session 为我们提供了3中存放的策略而每种策略提供对应的注解启动。分别为:
(1)NoSql形式的MongoDb:@EnableMongoHttpSession
(2)持久化形式的JDBC:@EnableJdbcHttpSession
(3)缓存形式的Redis:@EnableRedisHttpSession

3.Spring Session 共享Session过程:
(1)先过程过滤器存储将SessionID存放到本地的Cookie 和Redis中。
如果本地没有启用Cookie的情况下,Spring Session也就不能使用。
(2)获取Session的时候,先从请求中获取Session,Session不为空的情况下直接返回Session,若当前的Session为空的情况下,从Cookie中获取SessionId,判断SessionId不为空,再从Redis中获取Session,若从Redis中获取到的Session不为空将Session存放到请求中,再返回Session,如果从Redis中获取的Session为空,再创建新的Session并且添加到请求中,后返回Session。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容