之前寫過一篇nginx多tomcat負載均衡,主要記錄了使用nginx對多個tomcat 進行負載均衡,其實進行負載均衡之前還有一個問題沒有解決,那就是集群間的session共享,不然用戶在登錄網站之后session保存在tomcat A,但是下次訪問的時候nginx分發到了tomcat B,這個時候tomcat B沒有剛剛用戶登錄的session,所以用戶就失去了(本次)登錄狀態,下次訪問的時候nginx可能又分發到了tomcat A(其實通過配置可以給各個服務器分配權重,nginx根據權重來轉發到對應的服務器),用戶本次又是登錄的狀態了,這樣飄忽不定肯定是不行的,所以在進行集群負載均衡之前需要解決session共享的問題。
目錄
- 概述:簡述本次記錄的主要內容
- shiro的session:關於shiro的session管理
- 實現共享
- 總結
概述
因為項目中用到了shiro的權限控制,而且使用的是shiro的session,所以我就基於shiro的session管理基礎上對session進行多tomcat共享,共享的思路也很簡單,就是將session保存到數據庫,每個服務器在收到客戶端請求的時候都從數據庫中取,這樣就統一了多個服務器之間的session來源,實現了共享。只不過這里我使用的數據庫是redis。
shiro的session
之前在另外一篇博客(shiro實現APP、web統一登錄認證和權限管理)里面也提到了shiro的session問題,其實shiro的session只不過是基於認證的需要對tomcat的session進行了封裝,所以只要實現對shiro的session進行持久化就可以了,關於shiro的session管理,開濤老師的這一篇博客講得很清楚了(http://jinnianshilongnian.iteye.com/blog/2028675),可以參考這一篇博客來了解shiro對session的管理。
實現共享
在明白了shiro的session管理之后,我們就可以在此基礎上進行session的共享了,其實只需要繼承EnterpriseCacheSessionDAO(其實繼承CachingSessionDAO就可以了,但是這里考慮到集群中每次都訪問數據庫導致開銷過大,這里在本地使用ehcache進行緩存,每次讀取session的時候都先嘗試讀取本地ehcache緩存,沒有的話再去遠程redis數據庫中讀取),然后覆蓋原來的增刪改查操作,這樣多個服務器就共享了session,具體實現如下:
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.SimpleSession; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; public class SessionRedisDao extends EnterpriseCacheSessionDAO { // 創建session,保存到數據庫 @Override protected Serializable doCreate(Session session) { Serializable sessionId = super.doCreate(session); RedisDb.setObject(sessionId.toString().getBytes(), sessionToByte(session)); return sessionId; } // 獲取session @Override protected Session doReadSession(Serializable sessionId) { // 先從緩存中獲取session,如果沒有再去數據庫中獲取 Session session = super.doReadSession(sessionId); if(session == null){ byte[] bytes = RedisDb.getObject(sessionId.toString().getBytes()); if(bytes != null && bytes.length > 0){ session = byteToSession(bytes); } } return session; } // 更新session的最后一次訪問時間 @Override protected void doUpdate(Session session) { super.doUpdate(session); RedisDb.setObject(session.getId().toString().getBytes(), sessionToByte(session)); } // 刪除session @Override protected void doDelete(Session session) { super.doDelete(session); RedisDb.delString(session.getId() + ""); } // 把session對象轉化為byte保存到redis中 public byte[] sessionToByte(Session session){ ByteArrayOutputStream bo = new ByteArrayOutputStream(); byte[] bytes = null; try { ObjectOutputStream oo = new ObjectOutputStream(bo); oo.writeObject(session); bytes = bo.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return bytes; } // 把byte還原為session public Session byteToSession(byte[] bytes){ ByteArrayInputStream bi = new ByteArrayInputStream(bytes); ObjectInputStream in; SimpleSession session = null; try { in = new ObjectInputStream(bi); session = (SimpleSession) in.readObject(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return session; } }
上面的主要邏輯是實現session的管理,下面是和redis數據庫交互
import java.util.Arrays; import java.util.Date; import java.util.Set; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class RedisDb { private static JedisPool jedisPool; // session 在redis過期時間是30分鍾30*60 private static int expireTime = 1800; // 計數器的過期時間默認2天 private static int countExpireTime = 2*24*3600; private static String password = "123456"; private static String redisIp = "10.10.31.149"; private static int redisPort = 6379; private static int maxActive = 200; private static int maxIdle = 200; private static long maxWait = 5000; private static Logger logger = Logger.getLogger(RedisDb.class); static { initPool(); } // 初始化連接池 public static void initPool(){ JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(maxActive); config.setMaxIdle(maxIdle); config.setMaxWaitMillis(maxWait); config.setTestOnBorrow(false); jedisPool = new JedisPool(config, redisIp, redisPort, 10000, password); } // 從連接池獲取redis連接 public static Jedis getJedis(){ Jedis jedis = null; try{ jedis = jedisPool.getResource(); // jedis.auth(password); } catch(Exception e){ ExceptionCapture.logError(e); } return jedis; } // 回收redis連接 public static void recycleJedis(Jedis jedis){ if(jedis != null){ try{ jedis.close(); } catch(Exception e){ ExceptionCapture.logError(e); } } } // 保存字符串數據 public static void setString(String key, String value){ Jedis jedis = getJedis(); if(jedis != null){ try{ jedis.set(key, value); } catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // 獲取字符串類型的數據 public static String getString(String key){ Jedis jedis = getJedis(); String result = ""; if(jedis != null){ try{ result = jedis.get(key); }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } return result; } // 刪除字符串數據 public static void delString(String key){ Jedis jedis = getJedis(); if(jedis != null){ try{ jedis.del(key); }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // 保存byte類型數據 public static void setObject(byte[] key, byte[] value){ Jedis jedis = getJedis(); String result = ""; if(jedis != null){ try{ if(!jedis.exists(key)){ jedis.set(key, value); } // redis中session過期時間 jedis.expire(key, expireTime); } catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // 獲取byte類型數據 public static byte[] getObject(byte[] key){ Jedis jedis = getJedis(); byte[] bytes = null; if(jedis != null){ try{ bytes = jedis.get(key);; }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } return bytes; } // 更新byte類型的數據,主要更新過期時間 public static void updateObject(byte[] key){ Jedis jedis = getJedis(); if(jedis != null){ try{ // redis中session過期時間 jedis.expire(key, expireTime); }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // key對應的整數value加1 public static void inc(String key){ Jedis jedis = getJedis(); if(jedis != null){ try{ if(!jedis.exists(key)){ jedis.set(key, "1"); jedis.expire(key, countExpireTime); } else { // 加1 jedis.incr(key); } }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // 獲取所有keys public static Set<String> getAllKeys(String pattern){ Jedis jedis = getJedis(); if(jedis != null){ try{ return jedis.keys(pattern); }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } return null; } }
總結
這里只是實現了簡單的session共享,但是對session的管理還不夠全面,比如說session的驗證。其實通過tomcat容器本身就可以實現session共享,后面再詳細了解下tomcat對於session的管理。