轉博友的比較有用
參考文章:使用Spring Session做分佈式會話管理,分佈式應用session會話管理-基於redis
說在前面:由於這篇文章參考了別人的,自己也總結了,所以很不要臉的把這篇文章標爲了原創(捂臉)。但是參考的文章都寫在最上面了。
在Web項目開發中,會話管理是一個很重要的部分,用於存儲與用戶相關的數據。通常是由符合session規範的容器來負責存儲管理,也就是一旦容器關閉,重啓會導致會話失效。因此打造一個高可用性的系統,必須將session管理從容器中獨立出來。而這實現方案有很多種,下面簡單介紹下(僅限目前自己已知的):
第一種:使用硬件F5做粘性會話(不會,只是知道可以這麼幹),成本太高,後期也不好維護。
第二種:使用Nginx的ip_hash特性做粘性會話,可以讓用戶每次訪問都轉發到該用戶第一次訪問的web容器上,但是當該web容器上的應用掛了,nginx不會將該用戶的請求轉發到別的web容器,體驗不好。
第三種:基於cookie實現,將session直接存儲到cookie中(安全問題,雖然可以加密存儲,但是覺得不能將敏感數據放在客戶端)
第四種:使用容器擴展來實現,大家比較容易接受的是通過容器插件來實現,比如基於Tomcat的memcached-session-manager / tomcat-redis-session-manager,基於Jetty的jetty-nosql-memcache / jetty-session-redis等等。好處是對項目來說是透明的,無需改動代碼。不過前者目前還不支持Tomcat 8,或者說不太完善。此方法過於依賴容器,一旦容器升級或者更換意味着又得從新來過。並且代碼不在項目中,對開發者來說維護也是個問題(如果你使用的是weblogic、jboss、websphere等商業web容器,根本沒有這種第三方容器擴展,web容器換了廠商就不行了)。
第五種:自己實現一套會話管理,將session讀取/存儲到Redis或其他nosql或數據庫(網站用戶量大的情況下,頻繁dml數據,對db壓力大)中。很顯然這個方案靈活性最大,但開發需要一些額外的時間。弄懂了會挺不錯的。
第六種:使用框架的會話管理工具spring-session,可以理解是替換了Servlet那一套會話管理,既不依賴容器,又不需要改動代碼,並且是用了spring-data-redis那一套連接池,可以說是最完美的解決方案。當然,前提是項目要使用Spring Framework才行。
第七種:如果項目中使用shiro這種自帶session(非httpsession)的權限框架,需要重寫shiro的SessionDao將session存入redis(或其他)來實現session共享,可以自己重寫(也不是很麻煩)或者找現成的(shiro-redis微笑臉,省的自己造輪子了,知道原理就行)。
前三種方案:出於安全、成本(自己搞着玩不好弄)、無法完全做到7*24*365不宕機,個人不推薦。有興趣的自己研究吧。
剩下的四種方案大體思路其實都一樣,存到nosql中(內存讀寫快啊)。
各方案具體實現思路:
第四種:
直接去這幾個插件github地址,人家寫的使用方法更全,不在贅述。(看了點redis-session-manager的源碼,他是將自定義了一個RedisSession類繼承StandardSession類,這個類是tomcat中lib下的類,太依賴容器了,獲取session信息從redis中序列化/反序列化出來)
第五種:(參考別人博客,內容直接粘貼過來了,地址在最上面)
要實現將session信息存入redis,總結了下大概是三點:
1.實現httpsession接口,重寫我們需要用到的方法,比如set get這些。。balabala一堆......
2.繼承HttpServletRequestWrapper,這個類裏面有getSession()方法,我們需要重寫,來取自定義session
3.實現filter,用來攔截客戶端請求,獲取我們自定義的request,從而獲得session
具體實現(這個人並沒有實現從redis中讀取session的操作,需要將request中獲取session和產生session都加入redis操作,但是這裏的思路需要掌握,很受教,自己想要實現完美挺難的):
1.實現httpsession接口
- public class HttpSessionWrapper implements HttpSession {
- protected final Logger logger = LoggerFactory.getLogger(getClass());
- private String sid = "";
- private HttpServletRequest request;
- private HttpServletResponse response;
- private final long creationTime = System.currentTimeMillis();
- private final long lastAccessedTime = System.currentTimeMillis();
- private SessionMeta meta;
- public HttpSessionWrapper() {
- }
- public HttpSessionWrapper(String sid,SessionMeta meta, HttpServletRequest request,
- HttpServletResponse response) {
- this.sid=sid;
- this.request=request;
- this.response=response;
- this.meta=meta;
- }
- public Object getAttribute(String name) {
- logger.info(getClass()+"getAttribute(),name:"+name);
- Jedis jedis =null;
- Object obj =null;
- String jsonStr = null;
- try {
- jedis =JedisPoolStore.getInstance().getJedis(meta.getHost(), meta.getPort());
- jsonStr = jedis.get(name);
- if(jsonStr!=null||StringUtils.isNotEmpty(jsonStr)){
- jedis.expire(name, meta.getSeconds());// 重置過期時間
- obj =JSON.parseObject(jsonStr, User.class); //反序列對象
- }
- if (jedis != null) {
- JedisPoolStore.getInstance().returnJedis(jedis);
- }
- return obj;
- }
- catch (JSONException je) {
- logger.error(je.getMessage());
- if (null != jedis)
- JedisPoolStore.getInstance().returnJedis(jedis);
- return jsonStr;
- }
- catch (Exception e) {
- logger.error(e.getMessage());
- if (e instanceof JedisException) {
- if (null != jedis)
- JedisPoolStore.getInstance().returnBrokenJedis(jedis);
- } else {
- if (null != jedis)
- JedisPoolStore.getInstance().returnJedis(jedis);
- }
- throw new HttpSessionException(" session 異常 getAttribute() name:"+name);
- }
- }
- public void setAttribute(String name, Object value) {
- logger.info(getClass()+"setAttribute(),name:"+name);
- Jedis jedis =null;
- try {
- jedis =JedisPoolStore.getInstance().getJedis(meta.getHost(), meta.getPort());
- if(value instanceof String){
- String value_ =(String) value;
- jedis.set(name,value_);//普通字符串對象
- }else{
- jedis.set(name, JSON.toJSONString(value));//序列化對象
- }
- jedis.expire(name, meta.getSeconds());// 重置過期時間
- if (jedis != null) {
- JedisPoolStore.getInstance().returnJedis(jedis);
- }
- } catch (Exception e) {
- logger.error(e.getMessage());
- if (e instanceof JedisException) {
- if (null != jedis)
- JedisPoolStore.getInstance().returnBrokenJedis(jedis);
- } else {
- if (null != jedis)
- JedisPoolStore.getInstance().returnJedis(jedis);
- }
- throw new HttpSessionException(" session 異常 setAttribute() name:"+name+",value:"+value);
- }
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public void invalidate() {
- logger.info(getClass()+"invalidate()");
- }
- public void removeAttribute(String name) {
- logger.info(getClass()+"removeAttribute(),name:"+name);
- if(StringUtils.isNotEmpty(name)){
- Jedis jedis =null;
- try {
- jedis =JedisPoolStore.getInstance().getJedis(meta.getHost(), meta.getPort());
- jedis.del(name);
- if (jedis != null) {
- JedisPoolStore.getInstance().returnJedis(jedis);
- }
- } catch (Exception e) {
- logger.error(e.getMessage());
- if (e instanceof JedisException) {
- if (null != jedis)
- JedisPoolStore.getInstance().returnBrokenJedis(jedis);
- } else {
- if (null != jedis)
- JedisPoolStore.getInstance().returnJedis(jedis);
- }
- throw new HttpSessionException(" session 異常 removeAttribute() name:"+name);
- }
- }
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public Object getValue(String name) {
- return null;
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public Enumeration getAttributeNames() {
- return null;
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public String[] getValueNames() {
- return null;
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public void putValue(String name, Object value) {
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public void removeValue(String name) {
- }
- public long getCreationTime() {
- return creationTime;
- }
- public String getId() {
- logger.info(getClass()+"getId():"+sid);
- return sid;
- }
- public long getLastAccessedTime() {
- return lastAccessedTime;
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public ServletContext getServletContext() {
- return null;
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public void setMaxInactiveInterval(int interval) {
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public int getMaxInactiveInterval() {
- return 0;
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public HttpSessionContext getSessionContext() {
- return null;
- }
- /**
- * 不可用
- * @deprecated
- *
- */
- public boolean isNew() {
- logger.info(getClass()+"isNew()");
- return false;
- }
- }
- /***
- *
- * @author xiaoshuai
- *
- */
- public class DefinedHttpServletRequestWrapper extends HttpServletRequestWrapper{
- protected final Logger logger = LoggerFactory.getLogger(getClass());
- private HttpSessionWrapper currentSession;
- private HttpServletRequest request;
- private HttpServletResponse response;
- private String sid = "";
- private SessionMeta meta;
- public DefinedHttpServletRequestWrapper(HttpServletRequest request) {
- super(request);
- }
- public DefinedHttpServletRequestWrapper(String sid, HttpServletRequest request) {
- super(request);
- this.sid = sid;
- }
- public DefinedHttpServletRequestWrapper(String sid, SessionMeta meta,HttpServletRequest request,
- HttpServletResponse response) {
- super(request);
- this.request = request;
- this.response = response;
- this.sid = sid;
- this.meta=meta;
- }
- @Override
- public HttpSession getSession(boolean create) {
- if(currentSession != null) {
- return currentSession;
- }
- if(!create) {
- return null;
- }
- currentSession = new HttpSessionWrapper(sid,meta, request, response);
- return currentSession;
- }
- @Override
- public HttpSession getSession() {
- return getSession(true);
- }
- }
- public class SessionFilter implements Filter{
- protected final Logger logger = LoggerFactory.getLogger(getClass());
- private static SessionMeta meta = new SessionMeta();
- private static final String host ="host";
- private static final String port ="port";
- private static final String seconds="seconds";
- public void init(FilterConfig filterConfig) throws ServletException {
- logger.debug("init filterConfig info");
- meta.setHost(filterConfig.getInitParameter(host));
- meta.setPort(Integer.parseInt(filterConfig.getInitParameter(port)));
- meta.setSeconds(Integer.parseInt(filterConfig.getInitParameter(seconds)));
- }
- public void doFilter(ServletRequest request, ServletResponse response,
- FilterChain chain) throws IOException, ServletException {
- //從cookie中獲取sessionId,如果此次請求沒有sessionId,重寫爲這次請求設置一個sessionId
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- HttpServletResponse httpResponse = (HttpServletResponse) response;
- String sid = CookieHelper.findCookieInfo(httpRequest,CookieHelper.COOKIE_DOMAIN_NAME);
- if(StringUtils.isEmpty(sid) ){
- try {
- sid =CookieHelper.saveCookie(SessionId.doIds(), httpResponse);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- logger.info("JESSIONID:"+sid);
- chain.doFilter(new DefinedHttpServletRequestWrapper(sid,meta,httpRequest, httpResponse), response);
- }
- public void destroy() {
- }
- }
- <!-- session過濾器 -->
- <filter>
- <filter-name>sessionFilter</filter-name>
- <filter-class>cn.session.filter.SessionFilter</filter-class>
- <init-param>
- <param-name>host</param-name>
- <param-value>10.1.193.1</param-value>
- </init-param>
- <init-param>
- <param-name>port</param-name>
- <param-value>6372</param-value>
- </init-param>
- <init-param>
- <param-name>seconds</param-name>
- <param-value>1800</param-value>
- </init-param>
- </filter>
- <filter-mapping>
- <filter-name>sessionFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
- 這裏簡單記錄下整合的過程:
- 如果項目之前沒有整合過spring-data-redis的話,這一步需要先做,在maven中添加這兩個依賴:
- <dependency>
- <groupId>org.springframework.data</groupId>
- <artifactId>spring-data-redis</artifactId>
- <version>1.5.2.RELEASE</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.session</groupId>
- <artifactId>spring-session</artifactId>
- <version>1.0.2.RELEASE</version>
- </dependency>
- 再在applicationContext.xml中添加以下bean,用於定義redis的連接池和初始化redis模版操作類,自行替換其中的相關變量。
- <!-- redis -->
- <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
- </bean>
- <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
- <property name="hostName" value="${redis.host}" />
- <property name="port" value="${redis.port}" />
- <property name="password" value="${redis.pass}" />
- <property name="timeout" value="${redis.timeout}" />
- <property name="poolConfig" ref="jedisPoolConfig" />
- <property name="usePool" value="true" />
- </bean>
- <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
- <property name="connectionFactory" ref="jedisConnectionFactory" />
- </bean>
- <!-- 將session放入redis -->
- <bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
- <property name="maxInactiveIntervalInSeconds" value="1800" />
- </bean>
- 這裏前面幾個bean都是操作redis時候使用的,最後一個bean纔是spring-session需要用到的,其中的id可以不寫或者保持不變,這也是一個約定優先配置的體現。這個bean中又會自動產生多個bean,用於相關操作,極大的簡化了我們的配置項。其中有個比較重要的是springSessionRepositoryFilter,它將在下面的代理filter中被調用到。maxInactiveIntervalInSeconds表示超時時間,默認是1800秒。寫上述配置的時候我個人習慣採用xml來定義,官方文檔中有采用註解來聲明一個配置類。
- 然後是在web.xml中添加一個session代理filter,通過這個filter來包裝Servlet的getSession()。需要注意的是這個filter需要放在所有filter鏈最前面。
- <!-- delegatingFilterProxy -->
- <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>
- </filter-mapping>
- 這樣便配置完畢了,需要注意的是,spring-session要求Redis Server版本不低於2.8。
- 驗證:使用redis-cli就可以查看到session key了,且瀏覽器Cookie中的jsessionid已經替換爲session。
- 127.0.0.1:6379> KEYS *
- 1) "spring:session:expirations:1440922740000"
- 2) "spring:session:sessions:35b48cb4-62f8-440c-afac-9c7e3cfe98d3"
- 配置shiro的xml
- <!-- SessionManager -->
- <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
- <!-- session存儲的實現(此處爲自己重寫的sessionDao) -->
- <property name="sessionDAO" ref="redisSessionDAO" />
- <!-- 定時清理失效會話, 清理用戶直接關閉瀏覽器造成的孤立會話 -->
- <property name="sessionValidationInterval" value="1800000"/>
- <property name="sessionValidationSchedulerEnabled" value="true"/>
- <property name="sessionIdCookie" ref="sessionIdCookie"/>
- <property name="sessionIdCookieEnabled" value="true"/>
- </bean>
RedisSessionDao
- public class RedisSessionDao extends AbstractSessionDAO {
- private static Logger logger = LoggerFactory.getLogger(RedisSessionDao.class);
- /*
- * The Redis key prefix for the sessions
- */
- private static final String REDIS_SHIRO_SESSION = "shiro_redis_session:";
- private static final Integer SESSION_VAL_TIME_SPAN = 18000;
- /**
- * redis接口
- */
- @Resource(name = "cacheServiceImpl")
- private ICacheService iCacheService;
- /**
- * @discription 創建session,保存到數據庫
- *
- * @param session
- * @return
- *
- * @author zhaoqiang
- * @created 2017年3月3日 下午1:28:09
- */
- @Override
- protected Serializable doCreate(Session session) {
- Serializable sessionId = this.generateSessionId(session);
- this.assignSessionId(session, sessionId);
- saveSession(session);
- return sessionId;
- }
- /**
- * @discription 獲取session
- *
- * @param sessionId
- * @return
- *
- * @author zhaoqiang
- * @created 2017年3月3日 下午1:28:43
- */
- @Override
- protected Session doReadSession(Serializable sessionId) {
- if (sessionId == null) {
- throw new NullPointerException("session id is empty");
- }
- SimpleSession session = null;
- try {
- byte[] value = iCacheService.getByte(buildRedisSessionKey(sessionId));
- session = SerializeUtil.unserialize(value, SimpleSession.class);
- } catch (Exception e) {
- e.printStackTrace();
- logger.error("get session error");
- }
- return session;
- }
- /**
- * @discription 更新session的最後一次訪問時間
- *
- * @param session
- *
- * @author zhaoqiang
- * @created 2017年3月3日 下午1:34:24
- */
- @Override
- public void update(Session session) {
- saveSession(session);
- }
- /**
- * @discription 刪除session
- *
- * @param session
- *
- * @author zhaoqiang
- * @created 2017年3月3日 下午1:35:48
- */
- @Override
- public void delete(Session session) {
- if (session == null) {
- logger.error("session can not be null,delete failed");
- return;
- }
- Serializable id = session.getId();
- if (id == null) {
- throw new NullPointerException("session id is empty");
- }
- try {
- iCacheService.delByString(buildRedisSessionKey(id));
- } catch (Exception e) {
- e.printStackTrace();
- logger.error("delete session error");
- }
- }
- /**
- * @discription 獲得活躍的session集合
- *
- * @return
- *
- * @author zhaoqiang
- * @created 2017年3月3日 下午2:05:22
- */
- @Override
- public Collection<Session> getActiveSessions() {
- Set<Session> sessions = new HashSet<Session>();
- try {
- Set<String> keys = iCacheService.keys(REDIS_SHIRO_SESSION);
- if (keys == null || keys.size() == 0) {
- return sessions;
- }
- for (String key : keys) {
- Session s = null;
- try {
- s = iCacheService.getObject(key, SimpleSession.class);
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- sessions.add(s);
- }
- } catch (Exception e) {
- logger.error("[獲得活躍的session集合(getActiveSessions)-error][" + e.getMessage() + "]");
- e.printStackTrace();
- }
- return sessions;
- }
- /**
- * @discription 處理緩存中id名稱
- *
- * @param sessionId
- * @return
- *
- * @author zhaoqiang
- * @created 2017年3月9日 下午1:13:45
- */
- private String buildRedisSessionKey(Serializable sessionId) {
- return REDIS_SHIRO_SESSION + sessionId;
- }
- /**
- * @discription 持久化session到redis
- *
- * @param session
- *
- * @author zhaoqiang
- * @created 2017年3月9日 下午1:06:53
- */
- private void saveSession(Session session) {
- if (session == null || session.getId() == null) {
- throw new NullPointerException("session is empty");
- }
- try {
- String key = buildRedisSessionKey(session.getId());
- long sessionTimeOut = session.getTimeout() / 1000;
- Long expireTime = sessionTimeOut + SESSION_VAL_TIME_SPAN + (5 * 60);
- iCacheService.setObject(key, session, new Integer(expireTime.toString()));
- } catch (Exception e) {
- e.printStackTrace();
- logger.error("save session error");
- }
- }
- }