Redis解決老項目集羣Session共享案例與回顧

老項目突然之間客戶要用了而且用戶量還不少,後端移動端都需要給升級。第一改進的時候做了移動端與後端的服務分流,這次升級爲分佈式集羣模式。分佈式集羣模式需要解決Session共享問題和數據一致性分佈式鎖處理。因爲歷史原因,應用是單體應用並非微服務技術實現。爲應對移動端大概20000左右的用戶使用量做的如下改造。

目錄

服務器端口分配管理

分佈式集羣session共享管理

Tomcat session共享設置

業務系統Session管理

Nginx集羣配置管理

遇到的問題

回顧歷史版本改造改進處理


服務器端口分配管理

服務器作用與集羣部署的節點說明(實際應用過程中並沒有使用到這麼多集羣節點,規劃的多一點備用)。

分佈式集羣session共享管理

Tomcat session共享設置

做分佈式除了Token,一般需要解決Session共享問題。

 這一塊都是基於tomcat的改造細節就不多說了,實驗過程用的是mzd123的。配置說明:

1、修改Tomcat/config/redis-data-cache.properties  =====解決redissession同步問題

2、檢查Tomcat/lib是否存在commons-logging-1.2.jar、commons-pool2-2.4.2.jar、jedis-2.9.0.jar、tomcat-cluster-redis-session-manager-2.0.4.jar   =====解決redissession同步問題


3、設置Tomcat/config/context.xml    =====解決redissession同步問題

<!--redis管理session配置-->
	<Valve className="tomcat.request.session.redis.SessionHandlerValve"/>
    <Manager className="tomcat.request.session.redis.SessionManager"/>

節點可以看下Tomcat目錄結構:

業務系統Session管理

首先添加一個Session監聽監控應用系統Session創建和銷燬等操作,下面的代碼有其他業務湊合看吧:

SessionListener.java

package com.boonya.listener;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.apache.log4j.Logger;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.boonya.cache.XHTSystemConfig;
import com.boonya.webservice.util.RedisDistributedLock;
import com.boonya.webservice.util.RedisUtil;
import com.boonya.xht.util.Constants;
import data.common.util.StringUtils;

/**
 * 
 * @function 功能:系統Session管理
 * @author PJL
 * @package com.boonya.listener
 * @filename SessionListener.java
 * @time 2019年12月19日 下午6:08:03
 */
public class SessionListener implements HttpSessionListener,HttpSessionAttributeListener {
	
	private static Logger logger = Logger.getLogger(SessionListener.class);

	/**
	 * 保存當前登錄的所有用戶
	 */
	public static Map<HttpSession, String> loginUser = new ConcurrentHashMap<HttpSession, String>();
	
	/**
	 * 記錄當前在線用戶數量
	 */
	public static Long loginCount = 0L;
	
	/**
	 * 用這個作爲session中的key,userName 爲手機號
	 */
	public static String SESSION_LOGIN_NAME = "userName";
	

	/**
	 * session創建時監聽
	 */
	@Override
	public void sessionCreated(HttpSessionEvent arg0) {
		//logger.info("sessionCreated");
	}

	/**
	 * session銷燬時
	 */
	@Override
	public void sessionDestroyed(HttpSessionEvent arg0) {
		if (XHTSystemConfig.clusterModeForTomcat) {
			try {
				// 刪除共享REDIS SESSION KEY
				RedisUtil.expire(arg0.getSession().getId(), Constants.MOBILE_TOKEN_KEY_EXPIRED_NOW);
				// 刪除REDIS SESSION KEY
				String key=Constants.CLUSTER_USER_SESSION_KEY+arg0.getSession().getId();
				RedisUtil.expire(key, Constants.MOBILE_TOKEN_KEY_EXPIRED_NOW);
			} catch (Exception e) {
				e.printStackTrace();
			}finally{
				// 更新登錄session數量
				updateLoginUserCount();
			}
		} else {
			try {
				// 移除用戶session
				loginUser.remove(arg0.getSession());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

	}

	/**
	 * 添加屬性時
	 */
	@Override
	public void attributeAdded(HttpSessionBindingEvent arg0) {
		if (XHTSystemConfig.clusterModeForTomcat) {
			// 如果添加的屬性是用戶名, 則加入map中
			if (arg0.getName().equals(SESSION_LOGIN_NAME)) {
				try {
					// REDIS SESSION KEY
					String key=Constants.CLUSTER_USER_SESSION_KEY+arg0.getSession().getId();
					// 設置登錄session
					RedisUtil.hset(key, arg0.getSession().getId(), arg0.getValue().toString());
					RedisUtil.expire(key, Constants.MOBILE_TOKEN_KEY_ONE_HOUR);
					
					// 設置登錄用戶名
					String userName=(String) arg0.getSession().getAttribute(SESSION_LOGIN_NAME);
					String userNameKey=Constants.CLUSTER_USER_SESSION_USER+arg0.getSession().getId();
					RedisUtil.hset(userNameKey, userName,userName);
					RedisUtil.expire(userNameKey, Constants.MOBILE_TOKEN_KEY_ONE_HOUR);
					
					// 更新登錄session數量
					updateLoginUserCount();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		} else {
			// 如果添加的屬性是用戶名, 則加入map中
			if (arg0.getName().equals(SESSION_LOGIN_NAME)) {
				loginUser.put(arg0.getSession(), arg0.getValue().toString());
				loginCount++;
			}
		}
		

	}

	/**
	 * 移除屬性時
	 */
	@Override
	public void attributeRemoved(HttpSessionBindingEvent arg0) {
		if (XHTSystemConfig.clusterModeForTomcat) {
			// 如果添加的屬性是用戶名, 則加入map中
			if (arg0.getName().equals(SESSION_LOGIN_NAME)) {
				try {
					// REDIS SESSION KEY
					String key=Constants.CLUSTER_USER_SESSION_KEY+arg0.getSession().getId();
					// 設置登錄session立即失效
					RedisUtil.expire(key, Constants.MOBILE_TOKEN_KEY_EXPIRED_NOW);
					
					// 設置登錄用戶名
					String userNameKey=Constants.CLUSTER_USER_SESSION_USER+arg0.getSession().getId();
					RedisUtil.expire(userNameKey, Constants.MOBILE_TOKEN_KEY_EXPIRED_NOW);
					
					// 更新登錄session數量
					updateLoginUserCount();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		} else {
			// 如果移除的屬性是用戶名, 則從map中移除
			if (arg0.getName().equals(SESSION_LOGIN_NAME)) {
				try {
					loginUser.remove(arg0.getSession());
					loginCount--;
				} catch (Exception e) {
				}
			}
		}
	}

	/**
	 * 屬性更新時
	 */
	@Override
	public void attributeReplaced(HttpSessionBindingEvent arg0) {
		if (XHTSystemConfig.clusterModeForTomcat) {
			if (arg0.getName().equals(SESSION_LOGIN_NAME)) {
				try {
					// REDIS SESSION KEY
					String key=Constants.CLUSTER_USER_SESSION_KEY+arg0.getSession().getId();
					// 設置登錄session
					RedisUtil.hset(key, arg0.getSession().getId(), arg0.getValue().toString());
					RedisUtil.expire(key, Constants.MOBILE_TOKEN_KEY_ONE_HOUR);
					
					// 設置登錄用戶名
					String userName=(String) arg0.getSession().getAttribute(SESSION_LOGIN_NAME);
					String userNameKey=Constants.CLUSTER_USER_SESSION_USER+arg0.getSession().getId();
					RedisUtil.hset(userNameKey, userName,userName);
					RedisUtil.expire(userNameKey, Constants.MOBILE_TOKEN_KEY_ONE_HOUR);
				} catch (Exception e) {
					e.printStackTrace();
				}finally{
					// 更換新登錄用戶數量
					updateLoginUserCount();
				}
				
			}
		} else {
			if (arg0.getName().equals(SESSION_LOGIN_NAME)) {
				loginUser.put(arg0.getSession(), arg0.getValue().toString());
			}
		}
	}

	/**
	 * 判斷當前用戶是否已經登錄
	 * 
	 * @param userId
	 * @return
	 */
	public static boolean isLogonUser(String userName) {
		if (XHTSystemConfig.clusterModeForTomcat) {
			HttpServletRequest request=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
			String username=RedisUtil.hget(Constants.CLUSTER_USER_SESSION_USER+request.getSession().getId(), userName);
			if(StringUtils.IsNullOrEmpty(username)){
				return false;
			}
			return true;
		} else {
			Set<HttpSession> keys = SessionListener.loginUser.keySet();
			for (HttpSession key : keys) {
				if (SessionListener.loginUser.get(key).equals(userName)) {
					return true;
				}
			}
			return false;
		}

	}

	/**
	 * 判斷當前session是否已經登錄
	 * 
	 * @param hs
	 * @return
	 */
	public static boolean isLogonUser(HttpSession hs) {
		if (XHTSystemConfig.clusterModeForTomcat) {
			String sessionValue=RedisUtil.hget(Constants.CLUSTER_USER_SESSION_KEY+hs.getId(), hs.getId());
			if(StringUtils.IsNullOrEmpty(sessionValue)){
				return false;
			}
			return true;
		} else {
			Set<HttpSession> keys = SessionListener.loginUser.keySet();
			for (HttpSession key : keys) {
				if (key.equals(hs)) {
					return true;
				}
			}
			return false;
		}

	}

	/**
	 * 獲取當前登錄用戶的數量
	 * 
	 * @return
	 */
	public static Long getLoginUserCount() {
		if (XHTSystemConfig.clusterModeForTomcat) {
			// 獲取數量
			String key=Constants.CLUSTER_USER_SESSION_KEY.substring(0, Constants.CLUSTER_USER_SESSION_KEY.length()-1);
			String newKey=key+":*";
			// 模糊查詢
			Set<String> keys=RedisUtil.keys(newKey);
			Long userNumber=0L;
			if(null!=keys){
				userNumber=Long.valueOf( keys.size()+"");
			}
			return userNumber;
		} else {
			return loginCount;
		}
	}
	
	/**
	 * 更新用戶數量統計
	 * 
	 */
	public static long updateLoginUserCount(){
		final String requestId=UUID.randomUUID().toString();
		boolean success=RedisDistributedLock.tryGetDistributedLock(
				Constants.CLUSTER_APPLICATION_COUNT_LOCK, requestId,
				Constants.CLUSTER_APPLICATION_COUNT_LOCK_TIME);
		
		logger.error("更新USER COUNT分佈式鎖:"+success);
		// 獲取登錄session數量
		long countSession=getLoginUserCount();
		while(!success){
			try {
				Thread.sleep(100);
				logger.error("更新USER COUNT分佈式鎖:休眠1000ms!");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			success=RedisDistributedLock.tryGetDistributedLock(
					Constants.CLUSTER_APPLICATION_COUNT_LOCK, requestId,
					Constants.CLUSTER_APPLICATION_COUNT_LOCK_TIME);
			if(success){
				countSession=getLoginUserCount();
			    break;
			}
		}
		// 分佈式不宜進行加減運算
	    RedisUtil.set(Constants.CLUSTER_USER_SESSION_COUNT, countSession+"");
	    RedisDistributedLock.releaseDistributedLock(
	    		Constants.CLUSTER_APPLICATION_COUNT_LOCK, requestId);
		logger.error("釋放更新USER COUNT分佈式鎖成功!");
		return countSession;
	}

	/**
	 * 清除已經登錄用戶的緩存
	 * 
	 * @param userName
	 */
	@SuppressWarnings("rawtypes")
	public static void removeSession(String userName) {
		if (XHTSystemConfig.clusterModeForTomcat) {
			try {
				HttpServletRequest request=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
				// REDIS SESSION KEY
				String key=Constants.CLUSTER_USER_SESSION_KEY+request.getSession().getId();
				
				// 設置登錄session立即失效
				RedisUtil.expire(key, Constants.MOBILE_TOKEN_KEY_EXPIRED_NOW);
				
				// 更換新登錄用戶數量
				updateLoginUserCount();
			} catch (Exception e) {
				e.printStackTrace();
			}
		} else {
			Set<HttpSession> keys = SessionListener.loginUser.keySet();
			// 如果使用老remove方法則要用iterator遍歷。
			Iterator iterator = keys.iterator();
			while (iterator.hasNext()) {
				Object key = iterator.next();
				if (!StringUtils.IsNullOrEmpty(userName)
						&& userName.equals(SessionListener.loginUser.get(key))) {
					/** 這行代碼是關鍵。否則報concurrentModificationException **/
					/** 詳情原因見:http://www.cnblogs.com/dolphin0520/p/3933551.html **/
					iterator.remove();

					loginUser.remove(key);
					loginCount--;
				}
			}
		}
	}

}

注意:核心關注Session創建和銷燬。

web.xml配置監聽:

<listener>
		<listener-class>com.boonya.listener.SessionListener</listener-class> 
</listener>

Nginx集羣配置管理

106前置機內網外服務器Nginx配置(服務和代理在同一臺機器),之前有同事建議做每個集羣的虛擬機部署,因爲只是爲了驗收收錢不必要做的太複雜故而未予以採納。

http {
    include       mime.types;
    #default_type  application/octet-stream;
	default_type  text/html;
	
	charset utf-8;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;
	
	#nginx服務器與被代理服務連接超時時間,代理超時
	proxy_connect_timeout 300;

	#nginx服務器發送數據給被代理服務器超時時間,單位秒,
	#規定時間內nginx服務器沒發送數據,則超時
	proxy_send_timeout 300;

	#nginx服務器接收被代理服務器數據超時時間,單位秒,
	#規定時間內nginx服務器沒收到數據,則超時
	proxy_read_timeout 300;
	
	proxy_buffer_size 64k;
	proxy_buffers   8  128k;
	proxy_busy_buffers_size 128k;
	proxy_temp_file_write_size 128k;
	
 	# 客戶端請求頭設置
	client_header_buffer_size 10m;
	# 客戶端請求體過大設置
    client_max_body_size 128m;

    #gzip  on;
	gzip  on;
	
	#BS WEB集羣配置:服務器列表10
	upstream WebCluster{
		server localhost:9001 weight=1;
		server localhost:9002 weight=1;
		server localhost:9003 weight=1;
		server localhost:9004 weight=1;
		server localhost:9005 weight=1;
		server localhost:9006 weight=1;
		server localhost:9007 weight=1;
		server localhost:9008 weight=1;
		server localhost:9009 weight=1;
		server localhost:9010 weight=1;
	}
		
	#APP集羣配置:服務器列表3
	upstream AppCluster{
		server localhost:9021 weight=1;
		server localhost:9022 weight=1;
		server localhost:9023 weight=1;
	}

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;
		
		# 限請求數配置
        #limit_req_zone $binary_remote_addr zone=perip:20m rate=100r/s;
        #limit_req_zone $server_name zone=perserver:20m rate=10000r/s;
		
		 #移動端服務代理轉發配置IP地址請求適配
        location ^~ /webService/ {
            proxy_pass  http://AppCluster/webService/;
            proxy_redirect    off;
            proxy_set_header  Host $host;
            proxy_set_header  X-real-ip $remote_addr;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
            #limit_req zone=perip  burst=1000;
            #limit_req zone=perserver burst=100000;
        }
		
		# 集羣統一文件訪問路徑
		location ^~ /upload/ {
            alias  D:/application-images/upload/;
        }
		
		# 移動端APP下載代理
		location /forestryapp/ {
		   alias D:/forestryapp/;
		}
		
		
        # 默認訪問後臺管理系統服務
        location / {
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection "Upgrade";
            #root   html;
            #index  home.html index.html server.html;
			proxy_pass http://WebCluster;
        }
     #.....
   }
#.....
}

遇到的問題

1、連接池關閉問題:Jedis連接池未關閉導致Redis連接池、Tomcat線程資源耗盡,主要是引入了Redis分佈式鎖做用戶Session數量的統計而這部分代碼沒有關閉jedis連接,實際上沒必要加鎖,因爲可以直接讀取節點獲得數量,只是爲了從redis裏面直接看結果而做的輔助處理。

2、共享Session 移除問題:隱約覺得哪裏沒有做完就匆忙上線了,結果用戶的組織機構無法過濾,因爲共享session沒有移除即使單個Tomcat註銷了但是整個Redis管理的共享session並沒有被移除掉,只需要在session銷燬的地方移除掉共享Session的redis key。

3、集羣節點卡殼:沒有啓動的節點Nginx配置策略沒有完善導致輪詢過程中卡頓。

回顧歷史版本改造改進處理

1、第一次改造:移動端和後端分流。

2、第二次改造:MySQL數據庫索引解決查詢慢的問題(包括軌跡查詢、統計分析等)。

3、第三次改造:從內存方式全面轉向Redis讀取。

4、第四次改造:Redis主從方式,從節點只讀負責系統業務數據查看。

5、第五次改造:組織機構人員樹與地圖聯動分離後地圖聚合數據改造,分佈式集羣支持。

注:由於歷史原因組織機構人員樹加載要10幾秒,上萬節點後加上業務耦合到樹上計算基本樹就沒法用了。這次樹與地圖聯動分離頁面加載提升到1秒以內,並且得到了客戶的認可這是最可貴的,索引查詢數據5萬左右的可以做到毫秒級分頁加載。

發佈了625 篇原創文章 · 獲贊 534 · 訪問量 358萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章