Redis解决老项目集群Session共享案例与回顾

老项目突然之间客户要用了而且用户量还不少,后端移动端都需要给升级。第一改进的时候做了移动端与后端的服务分流,这次升级为分布式集群模式。分布式集群模式需要解决Session共享问题和数据一致性分布式锁处理。因为历史原因,应用是单体应用并非微服务技术实现。为应对移动端大概20000左右的用户使用量做的如下改造。

目录

服务器端口分配管理

分布式集群session共享管理

Tomcat session共享设置

业务系统Session管理

Nginx集群配置管理

遇到的问题

回顾历史版本改造改进处理


服务器端口分配管理

服务器作用与集群部署的节点说明(实际应用过程中并没有使用到这么多集群节点,规划的多一点备用)。

扫描二维码关注公众号,回复: 9520207 查看本文章

分布式集群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万左右的可以做到毫秒级分页加载。

发布了627 篇原创文章 · 获赞 535 · 访问量 359万+

猜你喜欢

转载自blog.csdn.net/boonya/article/details/103726430