1. 程式人生 > >【轉載】spring-session負載均衡原理分析

【轉載】spring-session負載均衡原理分析

註明轉載:https://www.jianshu.com/p/beaf18704c3c

第一部分:我會用循序漸進的方式來展示原始碼,從大家最熟悉的地方入手,而不是直接從系統啟動來debug原始碼。直接debug原始碼看到後來大家都會一頭霧水。 本文先從request.getSession()開始剖析原始碼,目標是讓讀者清楚的知曉Spring-session的產生過程。

第二部分:再上一部分Spring-session的產生過程的研究中如果讀者清楚了整個過程的脈絡,那麼肯定會產生一些疑惑:Servlet容器如何從預設的Session切換到Spring-session?為什麼request.getSession()會直接呼叫Spring的session管理方案?這一塊研究結束後整個Spring-session的大體原理分析就結束了。

剩下的就是其他一些策略的問題,篇幅有限,不再展開。讀者可以私下研究或者評論區域我們討論。比如

1.CookieHttpSessionStrategy和HeaderHttpSessionStrategy的區別
2.Session建立成功後儲存到session倉庫的具體過程?
...

那麼,先從第一部分開始

一. 提出問題假設

Spring-Session 的思路是替換Servlet容器提供的HttpSession。在web程式中通過呼叫方法 request.getSession() 生成session。Servlet容器裡面預設的request實現是HttpServletRequestWrapper

類。那麼為了替換原始的HttpSession,Spring-Session有兩種方案來重寫getSession()方法 :

1.實現`HttpServletRequest`介面
2.繼承`HttpServletRequestWrapper`類

我們從springmvc的controller進入request.getSession()方法,debug進去後發現getSession方法在這個類SessionRepositoryRequestWrapper,並且這個類繼承了HttpServletRequestWrapper。很開心有木有?驗證了我們上面的想法Spring-Session用第2種繼承的方式來實現HttpSession的自定義。

/*IndexController.java*/
    @Resource
    HttpServletRequest request;

    @RequestMapping({ "", "/index" })
    public String index(Model model) {
        HttpSession session = request.getSession();  //方法debug跟蹤
        Object user = session.getAttribute("curuser");
        if(user == null) return "redirect:login";
        model.addAttribute("port", request.getLocalPort());
        return "index";
    }
/*SessionRepositoryRequestWrapper.java*/

    @Override
    public HttpSessionWrapper getSession() {
        return getSession(true);
    }

大概的思路瞭然,那麼getSession(true)到底是如何運作的呢?getSession()這裡的業務也是最複雜的,存在各種狀態的判斷。開始研究getSession()。

二.在Controller中獲取Session

在controller中通過request.getSession()來獲取Session,下圖是此方法執行的過程。

 

image.png

@Override
public HttpSessionWrapper getSession(boolean create) {
    /*
    從request中獲取Session,首次訪問返回null
    其實這裡相當於request.getAttribute(key);
    在Session建立成功後會呼叫request.setAttribute(key,session);
    以便於在同一個request請求中直接獲取session
    */
    HttpSessionWrapper currentSession = getCurrentSession();
    if (currentSession != null) {
        return currentSession;
    }
    /*
    從Cookie或者header中獲取SESSIONID,如果我們用Cookie策略,這也是spring-session預設的。
    可以檢視瀏覽器cookie。存在鍵值對 SESSION:XXXXXXXXXXXXXXXX
    */
    String requestedSessionId = getRequestedSessionId();
    if (requestedSessionId != null
            && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
        /*
          根據上文得到的sessionid從Session倉庫中獲取Session
        */
        S session = getSession(requestedSessionId);
        if (session != null) {//有效的Session
            this.requestedSessionIdValid = true;
            currentSession = new HttpSessionWrapper(session, getServletContext());
            currentSession.setNew(false);
            setCurrentSession(currentSession);
            return currentSession;
        }else {//無效的session,
        
            if (SESSION_LOGGER.isDebugEnabled()) {
                SESSION_LOGGER.debug(
                "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
            }
            //Session無效,在request中增加一個鍵值對
            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)"));
    }
    /*
    首次訪問,則建立Session。
    */
    S session = SessionRepositoryFilter.this.sessionRepository.createSession();
    session.setLastAccessedTime(System.currentTimeMillis());
    currentSession = new HttpSessionWrapper(session, getServletContext());

    //將剛建立的session加入到request,以便於本次請求中再次getSession()時直接返回。
    setCurrentSession(currentSession);

    return currentSession;
}

至此,我們在controller中獲取到了Session。可以存取資料到Session裡面。在controller層response的時候把Session儲存到Session倉庫中(redis、mongo等)

三.spring-session與session是如何做到無縫切換的

web容器實現session共享的外掛也有,比如tomcat-redis-session-manager等,缺點比較多:需要在tomcat做配置,侵入嚴重。
Spring-session用了一個比較聰明又簡單的辦法

1.自定義一個Filter ,springSessionRepositoryFilter,攔截所有請求
2.繼承HttpServletRequestWrapper等類,重寫getSession()等方法。

這裡我們看看Spring官方文件

we can create our Spring configuration. The Spring configuration is responsible for creating a Servlet Filter that replaces the HttpSession implementation with an implementation backed by Spring Session. Add the following Spring Configuration:
(我們可以建立一個Spring 的配置,這個檔案是用來建立一個Filter,這個Filter裡面可以實現Spring session替換HttpSession的功能。Spring的配置如下)

XML實現方式

<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>

DelegatingFilterProxy這個類攔截每次請求,並且尋找到springSessionRepositoryFilter這個bean,並且將它轉換成Filter,用這個Filter處理每個request請求。

獲取springSessionRepositoryFilter這個bean。
Object obj = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()).getBean("springSessionRepositoryFilter");

debug檢視物件obj ,沒錯這就是spring-session最核心的Filter ——SessionReponsitoryFilter
[email protected]204ee

spring-session重寫的request(SessionRepositoryRequestWrapper),response(SessionRepositoryResponseWrapper)和Session(HttpSessionWrapper)都是SessionReponsitoryFilter類的內部類。第一部分著重說的getSession(boolean)方法就是在SessionRepositoryRequestWrapper這個類裡面重寫的。

註解實現方式

//@EnableRedisHttpSession這個註解建立了springSessionRepositoryFilter的Bean。
//並且建立了一個操作Redis的RedisConnectionFactory工廠類

@EnableRedisHttpSession 
public class Config {
        @Bean
        public LettuceConnectionFactory connectionFactory() {
                return new LettuceConnectionFactory(); 
        }
}

上面Config建立了Filter,接下來需要將這個Config載入到Spring。以此來實現每次請求過來首先經過這個Filter。

public class Initializer extends AbstractHttpSessionApplicationInitializer { 

        public Initializer() {
                super(Config.class); 
        }
}

那麼上面兩種配置方式裡的這個SessionReponsitoryFilter到底是啥樣的?這個Filter才是Spring-session的核心。我們來看看
SessionReponsitoryFilter 原始碼

@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends ExpiringSession>
        extends OncePerRequestFilter {
    private static final String SESSION_LOGGER_NAME = SessionRepositoryFilter.class
            .getName().concat(".SESSION_LOGGER");

    private static final Log SESSION_LOGGER = LogFactory.getLog(SESSION_LOGGER_NAME);

    /**
     * The session repository request attribute name.
     */
    public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class
            .getName();

    /**
     * Invalid session id (not backed by the session repository) request attribute name.
     */
    public static final String INVALID_SESSION_ID_ATTR = SESSION_REPOSITORY_ATTR
            + ".invalidSessionId";

    private static final String CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR
            + ".CURRENT_SESSION";

    /**
     * The default filter order.
     */
    public static final int DEFAULT_ORDER = Integer.MIN_VALUE + 50;

    private final SessionRepository<S> sessionRepository;

    private ServletContext servletContext;

    private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();

    /**
     * Creates a new instance.
     *
     * @param sessionRepository the <code>SessionRepository</code> to use. Cannot be null.
     */
    public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        if (sessionRepository == null) {
            throw new IllegalArgumentException("sessionRepository cannot be null");
        }
        this.sessionRepository = sessionRepository;
    }

    /**
     * Sets the {@link HttpSessionStrategy} to be used. The default is a
     * {@link CookieHttpSessionStrategy}.
     *
     * @param httpSessionStrategy the {@link HttpSessionStrategy} to use. Cannot be null.
     設定HttpSessionStrategy的策略,預設策略是CookieHttpSessionStrategy。表示從cookie中獲取sessionid。
     */
    public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
        if (httpSessionStrategy == null) {
            throw new IllegalArgumentException("httpSessionStrategy cannot be null");
        }
        this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(
                httpSessionStrategy);
    }

    /**
     * Sets the {@link MultiHttpSessionStrategy} to be used. The default is a
     * {@link CookieHttpSessionStrategy}.
     *
     * @param httpSessionStrategy the {@link MultiHttpSessionStrategy} to use. Cannot be
     * null.
     */
    public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {
        if (httpSessionStrategy == null) {
            throw new IllegalArgumentException("httpSessionStrategy cannot be null");
        }
        this.httpSessionStrategy = httpSessionStrategy;
    }
    /**
   這個方法是典型的模板方法設計模式的運用;SessionRepositoryFilter的父類定義了抽象方法doFilterInternal,並且在doFilter中呼叫,具體的實現丟給子類。

    */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        //封裝request和response
        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
                request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                wrappedRequest, response);
        //這裡的作用是通過方法request.setAttribute(HttpSessionManager.class.getName(), 策略);
        //把CookieHttpSessionStrategy加入到request。下面的response一樣
        HttpServletRequest strategyRequest = this.httpSessionStrategy
                .wrapRequest(wrappedRequest, wrappedResponse);
        HttpServletResponse strategyResponse = this.httpSessionStrategy
                .wrapResponse(wrappedRequest, wrappedResponse);

        try {
            filterChain.doFilter(strategyRequest, strategyResponse);
        }
        finally {
                        //這裡是response的時候把session加入到session倉庫(redis,MongoDB等),該方法在下面的SessionRepositoryRequestWrapper類
            wrappedRequest.commitSession();
        }
    }

    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    /**
     * Allows ensuring that the session is saved if the response is committed.
     *
     * @author Rob Winch
     * @since 1.0
     */
    private final class SessionRepositoryResponseWrapper
            extends OnCommittedResponseWrapper {

        private final SessionRepositoryRequestWrapper request;

        /**
         * Create a new {@link SessionRepositoryResponseWrapper}.
         * @param request the request to be wrapped
         * @param response the response to be wrapped
         */
        SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
                HttpServletResponse response) {
            super(response);
            if (request == null) {
                throw new IllegalArgumentException("request cannot be null");
            }
            this.request = request;
        }

        @Override
        protected void onResponseCommitted() {
            this.request.commitSession();
        }
    }

    /**
     * A {@link javax.servlet.http.HttpServletRequest} that retrieves the
     * {@link javax.servlet.http.HttpSession} using a
     * {@link org.springframework.session.SessionRepository}.
     *
     * @author Rob Winch
     * @since 1.0
     */
    private final class SessionRepositoryRequestWrapper
            extends HttpServletRequestWrapper {
        private Boolean requestedSessionIdValid;
        private boolean requestedSessionInvalidated;
        private final HttpServletResponse response;
        private final ServletContext servletContext;

        private SessionRepositoryRequestWrapper(HttpServletRequest request,
                HttpServletResponse response, ServletContext servletContext) {
            super(request);
            this.response = response;
            this.servletContext = servletContext;
        }

        /**
         * Uses the HttpSessionStrategy to write the session id to the response and
         * persist the Session.
         *  將session加入到session倉庫(redis,MongoDB等
         */
        private void commitSession() {
            HttpSessionWrapper wrappedSession = getCurrentSession();
            if (wrappedSession == null) {
                if (isInvalidateClientSession()) {
                    SessionRepositoryFilter.this.httpSessionStrategy
                            .onInvalidateSession(this, this.response);
                }
            }
            else {
                S session = wrappedSession.getSession();
                SessionRepositoryFilter.this.sessionRepository.save(session);
                if (!isRequestedSessionIdValid()
                        || !session.getId().equals(getRequestedSessionId())) {
                    SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
                            this, this.response);
                }
            }
        }
      //從當前request中獲取session
        @SuppressWarnings("unchecked")
        private HttpSessionWrapper getCurrentSession() {
            return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
        }
      //將session儲存到當前request請求中
        private void setCurrentSession(HttpSessionWrapper currentSession) {
            if (currentSession == null) {
                removeAttribute(CURRENT_SESSION_ATTR);
            }
            else {
                setAttribute(CURRENT_SESSION_ATTR, currentSession);
            }
        }

        @SuppressWarnings("unused")
        public String changeSessionId() {
            HttpSession session = getSession(false);

            if (session == null) {
                throw new IllegalStateException(
                        "Cannot change session ID. There is no session associated with this request.");
            }

            // eagerly get session attributes in case implementation lazily loads them
            Map<String, Object> attrs = new HashMap<String, Object>();
            Enumeration<String> iAttrNames = session.getAttributeNames();
            while (iAttrNames.hasMoreElements()) {
                String attrName = iAttrNames.nextElement();
                Object value = session.getAttribute(attrName);

                attrs.put(attrName, value);
            }

            SessionRepositoryFilter.this.sessionRepository.delete(session.getId());
            HttpSessionWrapper original = getCurrentSession();
            setCurrentSession(null);

            HttpSessionWrapper newSession = getSession();
            original.setSession(newSession.getSession());

            newSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
            for (Map.Entry<String, Object> attr : attrs.entrySet()) {
                String attrName = attr.getKey();
                Object attrValue = attr.getValue();
                newSession.setAttribute(attrName, attrValue);
            }
            return newSession.getId();
        }
  
        @Override
        public boolean isRequestedSessionIdValid() {
            if (this.requestedSessionIdValid == null) {
                String sessionId = getRequestedSessionId();
                S session = sessionId == null ? null : getSession(sessionId);
                return isRequestedSessionIdValid(session);
            }

            return this.requestedSessionIdValid;
        }

        private boolean isRequestedSessionIdValid(S session) {
            if (this.requestedSessionIdValid == null) {
                this.requestedSessionIdValid = session != null;
            }
            return this.requestedSessionIdValid;
        }

        private boolean isInvalidateClientSession() {
            return getCurrentSession() == null && this.requestedSessionInvalidated;
        }

        private S getSession(String sessionId) {
            S session = SessionRepositoryFilter.this.sessionRepository
                    .getSession(sessionId);
            if (session == null) {
                return null;
            }
            session.setLastAccessedTime(System.currentTimeMillis());
            return session;
        }

        @Override
        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;
        }

        @Override
        public ServletContext getServletContext() {
            if (this.servletContext != null) {
                return this.servletContext;
            }
            // Servlet 3.0+
            return super.getServletContext();
        }

        @Override
        public HttpSessionWrapper getSession() {
            return getSession(true);
        }
        //從session策略中獲取sessionid
        @Override
        public String getRequestedSessionId() {
            return SessionRepositoryFilter.this.httpSessionStrategy
                    .getRequestedSessionId(this);
        }

        /**
         * Allows creating an HttpSession from a Session instance.
         *
         * @author Rob Winch
         * @since 1.0
         */
        private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {

            HttpSessionWrapper(S session, ServletContext servletContext) {
                super(session, servletContext);
            }

            @Override
            public void invalidate() {
                super.invalidate();
                SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
                setCurrentSession(null);
                SessionRepositoryFilter.this.sessionRepository.delete(getId());
            }
        }
    }

    /**
     * A delegating implementation of {@link MultiHttpSessionStrategy}.
     */
    static class MultiHttpSessionStrategyAdapter implements MultiHttpSessionStrategy {
        private HttpSessionStrategy delegate;

        /**
         * Create a new {@link MultiHttpSessionStrategyAdapter} instance.
         * @param delegate the delegate HTTP session strategy
         */
        MultiHttpSessionStrategyAdapter(HttpSessionStrategy delegate) {
            this.delegate = delegate;
        }

        public String getRequestedSessionId(HttpServletRequest request) {
            return this.delegate.getRequestedSessionId(request);
        }

        public void onNewSession(Session session, HttpServletRequest request,
                HttpServletResponse response) {
            this.delegate.onNewSession(session, request, response);
        }

        public void onInvalidateSession(HttpServletRequest request,
                HttpServletResponse response) {
            this.delegate.onInvalidateSession(request, response);
        }

        public HttpServletRequest wrapRequest(HttpServletRequest request,
                HttpServletResponse response) {
            return request;
        }

        public HttpServletResponse wrapResponse(HttpServletRequest request,
                HttpServletResponse response) {
            return response;
        }
    }
}

結語

spring-session原始碼的解讀就這麼粗糙的結束了,一些狀態判斷性的原始碼沒有解讀。我相信只要讀者把主線業務整理明白了,其他方法小菜一碟。