老項目突然之間客戶要用了而且用戶量還不少,後端移動端都需要給升級。第一改進的時候做了移動端與後端的服務分流,這次升級爲分佈式集羣模式。分佈式集羣模式需要解決Session共享問題和數據一致性分佈式鎖處理。因爲歷史原因,應用是單體應用並非微服務技術實現。爲應對移動端大概20000左右的用戶使用量做的如下改造。
目錄
服務器端口分配管理
服務器作用與集羣部署的節點說明(實際應用過程中並沒有使用到這麼多集羣節點,規劃的多一點備用)。
分佈式集羣session共享管理
Tomcat session共享設置
做分佈式除了Token,一般需要解決Session共享問題。
- Tomcat的session共享Redis方案可以參考:https://github.com/mzd123/session_manager
- 國外開源項目: https://github.com/jcoleman/tomcat-redis-session-manager
- 推薦開源項目支持Tomcat789: https://github.com/ran-jit/tomcat-cluster-redis-session-manager
這一塊都是基於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萬左右的可以做到毫秒級分頁加載。