前言:
由于項目需求,需要在集群環境下實現在線用戶列表的功能,并依靠在線列表實現用戶單一登陸(同一賬戶只能一處登陸)功能:
在單機環境下,在線列表的實現方案可以采用SessionListener來完成,當有Session創建和銷毀的時候做相應的操作即可完成功能及將相應的Session的引用存放于內存中,由于持有了所有的Session的引用,故可以方便的實現用戶單一登陸的功能(比如在第二次登陸的時候使之前登陸的賬戶所在的Session失效)。
而在集群環境下,由于用戶的請求可能分布在不同的Web服務器上,繼續將在線用戶列表儲存在單機內存中已經不能滿足需要,不同的Web服務器將會產生不同的在線列表,并且不能有效的實現單一用戶登陸的功能,因為某一用戶可能并不在接受到退出請求的Web服務器的在線用戶列表中(在集群中的某臺服務器上完成的登陸操作,而在其他服務器上完成退出操作)。
現有解決方案:
1.將用戶的在線情況記錄進入數據庫中,依靠數據庫完成對登陸狀況的檢測
2.將在線列表放在一個公共的緩存服務器上
由于緩存服務器可以為緩存內容設置指定有效期,可以方便實現Session過期的效果,以及避免讓數據庫的讀寫性能成為系統瓶頸等原因,我們采用了Redis來作為緩存服務器用于實現該功能。
單機環境下的解決方案:
基于HttpSessionListener:
1 import java.util.Date; 2 import java.util.Hashtable; 3 import java.util.Iterator; 4 import javax.servlet.http.HttpSession; 5 import javax.servlet.http.HttpSessionEvent; 6 import javax.servlet.http.HttpSessionListener; 7 import com.xxx.common.util.StringUtil; 8 /** 9 * 10 * @ClassName: SessionListener 11 * @Description: 記錄所有登陸的Session信息,為在線列表做基礎 12 * @author libaoting 13 * @date 2013-9-18 09:35:13 14 * 15 */ 16 public class SessionListener implements HttpSessionListener { 17 // 在線列表<uid,session> 18 private static Hashtable<String,HttpSession> sessionList = new Hashtable<String, HttpSession> (); 19 public void sessionCreated(HttpSessionEvent event) { 20 // 不做處理,只處理登陸用戶的列表 21 } 22 public void sessionDestroyed(HttpSessionEvent event) { 23 removeSession(event.getSession()); 24 } 25 public static void removeSession(HttpSession session){ 26 if (session == null ){ 27 return ; 28 } 29 String uid=(String)session.getAttribute("clientUserId"); // 已登陸狀態會將用戶的UserId保存在session中 30 if (!StringUtil.isBlank(uid)){ // 判斷是否登陸狀態 31 removeSession(uid); 32 } 33 } 34 public static void removeSession(String uid){ 35 HttpSession session = sessionList.get(uid); 36 try { 37 sessionList.remove(uid); // 先執行,防止session.invalidate()報錯而不執行 38 if (session != null ){ 39 session.invalidate(); 40 } 41 } catch (Exception e) { 42 System.out.println("Session invalidate error!" ); 43 } 44 } 45 public static void addSession(String uid,HttpSession session){ 46 sessionList.put(uid, session); 47 } 48 public static int getSessionCount(){ 49 return sessionList.size(); 50 } 51 public static Iterator<HttpSession> getSessionSet(){ 52 return sessionList.values().iterator(); 53 } 54 public static HttpSession getSession(String id){ 55 return sessionList.get(id); 56 } 57 public static boolean contains(String uid){ 58 return sessionList.containsKey(uid); 59 } 60 /** 61 * 62 * @Title: isLoginOnThisSession 63 * @Description: 檢測是否已經登陸 64 * @param @param uid 用戶UserId 65 * @param @param sid 發起請求的用戶的SessionId 66 * @return boolean true 校驗通過 67 */ 68 public static boolean isLoginOnThisSession(String uid,String sid){ 69 if (uid== null ||sid== null ){ 70 return false ; 71 } 72 if (contains(uid)){ 73 HttpSession session = sessionList.get(uid); 74 if (session!= null && session.getId().equals(sid)){ 75 return true ; 76 } 77 } 78 return false ; 79 } 80 }
用戶的在線狀態全部維護記錄在sessionList中,并且可以通過sessionList獲取到任意用戶的session對象,可以用來完成使指定用戶離線的功能(調用該用戶的session.invalidate()方法)。
用戶登錄的時候調用addSession(uid,session)方法將用戶與其登錄的Session信息記錄至sessionList中,再退出的時候調用removeSession(session) or removeSession(uid)方法,在強制下線的時候調用removeSession(uid)方法,以及一些其他的操作即可實現相應的功能。
基于Redis的解決方案:
該解決方案的實質是將在線列表的所在的內存共享出來,讓集群環境下所有的服務器都能夠訪問到這部分數據,并且將用戶的在線狀態在這塊內存中進行維護。
Redis連接池工具類:
1 import java.util.ResourceBundle; 2 import redis.clients.jedis.Jedis; 3 import redis.clients.jedis.JedisPool; 4 import redis.clients.jedis.JedisPoolConfig; 5 public class RedisPoolUtils { 6 private static final JedisPool pool; 7 static { 8 ResourceBundle bundle = ResourceBundle.getBundle("redis" ); 9 JedisPoolConfig config = new JedisPoolConfig(); 10 if (bundle == null ) { 11 throw new IllegalArgumentException("[redis.properties] is not found!" ); 12 } 13 // 設置池配置項值 14 config.setMaxActive(Integer.valueOf(bundle.getString("jedis.pool.maxActive" ))); 15 config.setMaxIdle(Integer.valueOf(bundle.getString("jedis.pool.maxIdle" ))); 16 config.setMaxWait(Long.valueOf(bundle.getString("jedis.pool.maxWait" ))); 17 config.setTestOnBorrow(Boolean.valueOf(bundle.getString("jedis.pool.testOnBorrow" ))); 18 config.setTestOnReturn(Boolean.valueOf(bundle.getString("jedis.pool.testOnReturn" ))); 19 pool = new JedisPool(config, bundle.getString("redis.ip"),Integer.valueOf(bundle.getString("redis.port" )) ); 20 } 21 /** 22 * 23 * @Title: release 24 * @Description: 釋放連接 25 * @param @param jedis 26 * @return void 27 * @throws 28 */ 29 public static void release(Jedis jedis){ 30 pool.returnResource(jedis); 31 } 32 public static Jedis getJedis(){ 33 return pool.getResource(); 34 } 35 } 36 Redis在線列表工具類: 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.Comparator; 40 import java.util.Date; 41 import java.util.List; 42 import java.util.Set; 43 import net.sf.json.JSONObject; 44 import net.sf.json.JsonConfig; 45 import net.sf.json.processors.JsonValueProcessor; 46 import cn.sccl.common.util.StringUtil; 47 import com.xxx.common.util.JsonDateValueProcessor; 48 import com.xxx.user.model.ClientUser; 49 import redis.clients.jedis.Jedis; 50 import redis.clients.jedis.Pipeline; 51 import tools.Constants; 52 /** 53 * 54 * Redis緩存中存放兩組key: 55 * 1.SID_PREFIX開頭,存放登陸用戶的SessionId與ClientUser的Json數據 56 * 2.UID_PREFIX開頭,存放登錄用戶的UID與SessionId對于的數據 57 * 58 * 3.VID_PREFIX開頭,存放位于指定頁面用戶的數據(與Ajax一起使用,用于實現指定頁面同時瀏覽人數的限制功能) 59 * 60 * @ClassName: OnlineUtils 61 * @Description: 在線列表操作工具類 62 * @author BuilderQiu 63 * @date 2014-1-9 上午09:25:43 64 * 65 */ 66 public class OnlineUtils { 67 // KEY值根據SessionID生成 68 private static final String SID_PREFIX = "online:sid:" ; 69 private static final String UID_PREFIX = "online:uid:" ; 70 private static final String VID_PREFIX = "online:vid:" ; 71 private static final int OVERDATETIME = 30 * 60 ; 72 private static final int BROADCAST_OVERDATETIME = 70; // ax每60秒發起一次,超過BROADCAST_OVERDATETIME時間長度未發起表示已經離開該頁面 73 public static void login(String sid,ClientUser user){ 74 Jedis jedis = RedisPoolUtils.getJedis(); 75 jedis.setex(SID_PREFIX+ sid, OVERDATETIME, userToString(user)); 76 jedis.setex(UID_PREFIX+ user.getId(), OVERDATETIME, sid); 77 RedisPoolUtils.release(jedis); 78 } 79 public static void broadcast(String uid,String identify){ 80 if (uid== null ||"".equals(uid)) // 異常數據,正常情況下登陸用戶才會發起該請求 81 return ; 82 Jedis jedis = RedisPoolUtils.getJedis(); 83 jedis.setex(VID_PREFIX+identify+":"+ uid, BROADCAST_OVERDATETIME, uid); 84 RedisPoolUtils.release(jedis); 85 } 86 private static String userToString(ClientUser user){ 87 JsonConfig config = new JsonConfig(); 88 JsonValueProcessor processor = new JsonDateValueProcessor("yyyy-MM-dd HH:mm:ss" ); 89 config.registerJsonValueProcessor(Date. class , processor); 90 JSONObject obj = JSONObject.fromObject(user, config); 91 return obj.toString(); 92 } 93 /** 94 * 95 * @Title: logout 96 * @Description: 退出 97 * @param @param sessionId 98 * @return void 99 * @throws 100 */ 101 public static void logout(String sid,String uid){ 102 Jedis jedis = RedisPoolUtils.getJedis(); 103 jedis.del(SID_PREFIX+ sid); 104 jedis.del(UID_PREFIX+ uid); 105 RedisPoolUtils.release(jedis); 106 } 107 /** 108 * 109 * @Title: logout 110 * @Description: 退出 111 * @param @param UserId 使指定用戶下線 112 * @return void 113 * @throws 114 */ 115 public static void logout(String uid){ 116 Jedis jedis = RedisPoolUtils.getJedis(); 117 // 刪除sid 118 jedis.del(SID_PREFIX+jedis.get(UID_PREFIX+ uid)); 119 // 刪除uid 120 jedis.del(UID_PREFIX+ uid); 121 RedisPoolUtils.release(jedis); 122 } 123 public static String getClientUserBySessionId(String sid){ 124 Jedis jedis = RedisPoolUtils.getJedis(); 125 String user = jedis.get(SID_PREFIX+ sid); 126 RedisPoolUtils.release(jedis); 127 return user; 128 } 129 public static String getClientUserByUid(String uid){ 130 Jedis jedis = RedisPoolUtils.getJedis(); 131 String user = jedis.get(SID_PREFIX+jedis.get(UID_PREFIX+ uid)); 132 RedisPoolUtils.release(jedis); 133 return user; 134 } 135 /** 136 * 137 * @Title: online 138 * @Description: 所有的key 139 * @return List 140 * @throws 141 */ 142 public static List online(){ 143 Jedis jedis = RedisPoolUtils.getJedis(); 144 Set online = jedis.keys(SID_PREFIX+"*" ); 145 RedisPoolUtils.release(jedis); 146 return new ArrayList(online); 147 } 148 /** 149 * 150 * @Title: online 151 * @Description: 分頁顯示在線列表 152 * @return List 153 * @throws 154 */ 155 public static List onlineByPage( int page, int pageSize) throws Exception{ 156 Jedis jedis = RedisPoolUtils.getJedis(); 157 Set onlineSet = jedis.keys(SID_PREFIX+"*" ); 158 List onlines = new ArrayList(onlineSet); 159 if (onlines.size() == 0 ){ 160 return null ; 161 } 162 Pipeline pip = jedis.pipelined(); 163 for (Object key:onlines){ 164 pip.get(getKey(key)); 165 } 166 List result = pip.syncAndReturnAll(); 167 RedisPoolUtils.release(jedis); 168 List<ClientUser> listUser= new ArrayList<ClientUser> (); 169 for ( int i=0;i<result.size();i++ ){ 170 listUser.add(Constants.json2ClientUser((String)result.get(i))); 171 } 172 Collections.sort(listUser, new Comparator<ClientUser> (){ 173 public int compare(ClientUser o1, ClientUser o2) { 174 return o2.getLastLoginTime().compareTo(o1.getLastLoginTime()); 175 } 176 }); 177 onlines= listUser; 178 int start = (page - 1) * pageSize; 179 int toIndex=(start+pageSize)>onlines.size()?onlines.size():start+ pageSize; 180 List list = onlines.subList(start, toIndex); 181 return list; 182 } 183 private static String getKey(Object obj){ 184 String temp = String.valueOf(obj); 185 String key[] = temp.split(":" ); 186 return SID_PREFIX+key[key.length-1 ]; 187 } 188 /** 189 * 190 * @Title: onlineCount 191 * @Description: 總在線人數 192 * @param @return 193 * @return int 194 * @throws 195 */ 196 public static int onlineCount(){ 197 Jedis jedis = RedisPoolUtils.getJedis(); 198 Set online = jedis.keys(SID_PREFIX+"*" ); 199 RedisPoolUtils.release(jedis); 200 return online.size(); 201 } 202 /** 203 * 獲取指定頁面在線人數總數 204 */ 205 public static int broadcastCount(String identify) { 206 Jedis jedis = RedisPoolUtils.getJedis(); 207 Set online = jedis.keys(VID_PREFIX+identify+":*" ); 208 RedisPoolUtils.release(jedis); 209 return online.size(); 210 } 211 /** 212 * 自己是否在線 213 */ 214 public static boolean broadcastIsOnline(String identify,String uid) { 215 Jedis jedis = RedisPoolUtils.getJedis(); 216 String online = jedis.get(VID_PREFIX+identify+":"+ uid); 217 RedisPoolUtils.release(jedis); 218 return !StringUtil.isBlank(online); // 不為空就代表已經找到數據了,也就是上線了 219 } 220 /** 221 * 獲取指定頁面在線人數總數 222 */ 223 public static int broadcastCount() { 224 Jedis jedis = RedisPoolUtils.getJedis(); 225 Set online = jedis.keys(VID_PREFIX+"*" ); 226 RedisPoolUtils.release(jedis); 227 return online.size(); 228 } 229 /** 230 * 231 * @Title: isOnline 232 * @Description: 指定賬號是否登陸 233 * @param @param sessionId 234 * @param @return 235 * @return boolean 236 * @throws 237 */ 238 public static boolean isOnline(String uid){ 239 Jedis jedis = RedisPoolUtils.getJedis(); 240 boolean isLogin = jedis.exists(UID_PREFIX+ uid); 241 RedisPoolUtils.release(jedis); 242 return isLogin; 243 } 244 public static boolean isOnline(String uid,String sid){ 245 Jedis jedis = RedisPoolUtils.getJedis(); 246 String loginSid = jedis.get(UID_PREFIX+ uid); 247 RedisPoolUtils.release(jedis); 248 return sid.equals(loginSid); 249 } 250 }
由于在線狀態是記錄在Redis中的,并不單純依靠Session的過期機制來實現,所以需要通過攔截器在每次發送請求的時候去更新Redis中相應的緩存過期時間來更新用戶的在線狀態。
登陸、退出操作與單機版相似,強制下線需要配合攔截器實現,當用戶下次訪問的時候,自己來校驗自己的狀態是否為已經下線,不再由服務器控制。
配合攔截器實現在線狀態維持與強制登陸(使其他地方登陸了該賬戶的用戶下線)功能:
1 ... 2 if (uid != null ){ // 已登錄 3 if (! OnlineUtils.isOnline(uid, session.getId())){ 4 session.invalidate(); 5 return ai.invoke(); 6 } else { 7 OnlineUtils.login(session.getId(), (ClientUser)session.getAttribute("clientUser" )); 8 // 刷新緩存 9 } 10 } 11 ...
注:Redis在線列表工具類中的部分代碼是后來需要實現限制同時訪問指定頁面瀏覽人數功能而添加的,同樣基于Redis實現,前端由Ajax輪詢來更新用戶停留頁面的狀態。
附錄:
Redis連接池配置文件:
###redis##config######## #redis服務器ip # #redis.ip= localhost #redis服務器端口號# redis .port=6379 ###jedis##pool##config### #jedis的最大分配對象# jedis .pool.maxActive=1024 #jedis最大保存idel狀態對象數 # jedis .pool.maxIdle=200 #jedis池沒有對象返回時,最大等待時間 # jedis .pool. maxWait =1000 #jedis調用borrowObject方法時,是否進行有效檢查# jedis .pool.testOnBorrow= true #jedis調用returnObject方法時,是否進行有效檢查 # jedis .pool.testOnReturn=true
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
