[項目回顧]基於Redis的在線用戶列表解決方案


遷移:基於Redis的在線用戶列表解決方案

前言:

  由於項目需求,需要在集群環境下實現在線用戶列表的功能,並依靠在線列表實現用戶單一登陸(同一賬戶只能一處登陸)功能:

  在單機環境下,在線列表的實現方案可以采用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 
 5 import javax.servlet.http.HttpSession;
 6 import javax.servlet.http.HttpSessionEvent;
 7 import javax.servlet.http.HttpSessionListener;
 8 
 9 import com.xxx.common.util.StringUtil;
10 
11 /**
12  * 
13  * @ClassName: SessionListener
14  * @Description: 記錄所有登陸的Session信息,為在線列表做基礎
15  * @author BuilderQiu
16  * @date 2013-9-18 09:35:13
17  *
18  */
19 public class SessionListener implements HttpSessionListener {
20     
21     //在線列表<uid,session>
22     private static Hashtable<String,HttpSession> sessionList = new Hashtable<String, HttpSession>();
23     
24 
25     public void sessionCreated(HttpSessionEvent event) {
26         //不做處理,只處理登陸用戶的列表
27         
28     }
29 
30     public void sessionDestroyed(HttpSessionEvent event) {
31         removeSession(event.getSession());
32     }
33     
34     public static void removeSession(HttpSession session){
35         if(session == null){
36             return ;
37         }
38 
39         String uid=(String)session.getAttribute("clientUserId");//已登陸狀態會將用戶的UserId保存在session中
40         if(!StringUtil.isBlank(uid)){//判斷是否登陸狀態
41             removeSession(uid);
42         }
43     }
44     
45     public static void removeSession(String uid){
46         HttpSession session = sessionList.get(uid);
47         try{
48             sessionList.remove(uid);//先執行,防止session.invalidate()報錯而不執行
49             if(session != null){
50                 session.invalidate();
51             }
52         }catch (Exception e) {
53             System.out.println("Session invalidate error!");
54         }
55     }
56     
57     public static void addSession(String uid,HttpSession session){
58         sessionList.put(uid, session);
59     }
60     
61     public static int getSessionCount(){
62         return sessionList.size();
63     }
64     
65     public static Iterator<HttpSession> getSessionSet(){
66         return sessionList.values().iterator();
67     }
68     
69     public static HttpSession getSession(String id){
70         return sessionList.get(id);
71     }
72     
73     public static boolean contains(String uid){
74         return sessionList.containsKey(uid);
75     }
76     
77     /**
78      * 
79      * @Title: isLoginOnThisSession
80      * @Description: 檢測是否已經登陸
81      * @param @param uid 用戶UserId
82      * @param @param sid 發起請求的用戶的SessionId
83      * @return boolean true 校驗通過 
84      */
85     public static boolean isLoginOnThisSession(String uid,String sid){
86         if(uid==null||sid==null){
87             return false;
88         }
89         if(contains(uid)){
90             HttpSession session = sessionList.get(uid);
91             
92             if(session!=null&&session.getId().equals(sid)){
93                 return true;
94             }
95         }
96         return false;
97     }
98     
99 }

  用戶的在線狀態全部維護記錄在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 
 3 import redis.clients.jedis.Jedis;
 4 import redis.clients.jedis.JedisPool;
 5 import redis.clients.jedis.JedisPoolConfig;
 6 
 7 public class RedisPoolUtils {
 8     
 9     private static final JedisPool pool;
10     
11     static{
12         ResourceBundle bundle = ResourceBundle.getBundle("redis");
13         JedisPoolConfig config = new JedisPoolConfig();
14         if (bundle == null) {    
15             throw new IllegalArgumentException("[redis.properties] is not found!");    
16         }
17         //設置池配置項值  
18         config.setMaxActive(Integer.valueOf(bundle.getString("jedis.pool.maxActive")));    
19         config.setMaxIdle(Integer.valueOf(bundle.getString("jedis.pool.maxIdle")));    
20         config.setMaxWait(Long.valueOf(bundle.getString("jedis.pool.maxWait")));    
21         config.setTestOnBorrow(Boolean.valueOf(bundle.getString("jedis.pool.testOnBorrow")));    
22         config.setTestOnReturn(Boolean.valueOf(bundle.getString("jedis.pool.testOnReturn")));
23         
24         pool = new JedisPool(config, bundle.getString("redis.ip"),Integer.valueOf(bundle.getString("redis.port")) );
25     }
26     
27     /**
28      * 
29      * @Title: release
30      * @Description: 釋放連接
31      * @param @param jedis
32      * @return void
33      * @throws
34      */
35     public static void release(Jedis jedis){
36         pool.returnResource(jedis);
37     }
38     
39     public static Jedis getJedis(){
40         return pool.getResource();
41     }
42 
43 }

  Redis在線列表工具類:

  1 import java.util.ArrayList;
  2 import java.util.Collections;
  3 import java.util.Comparator;
  4 import java.util.Date;
  5 import java.util.List;
  6 import java.util.Set;
  7 
  8 import net.sf.json.JSONObject;
  9 import net.sf.json.JsonConfig;
 10 import net.sf.json.processors.JsonValueProcessor;
 11 
 12 import cn.sccl.common.util.StringUtil;
 13 
 14 import com.xxx.common.util.JsonDateValueProcessor;
 15 import com.xxx.user.model.ClientUser;
 16 
 17 import redis.clients.jedis.Jedis;
 18 import redis.clients.jedis.Pipeline;
 19 import tools.Constants;
 20 
 21 /**
 22  * 
 23  * Redis緩存中存放兩組key:
 24  * 1.SID_PREFIX開頭,存放登陸用戶的SessionId與ClientUser的Json數據
 25  * 2.UID_PREFIX開頭,存放登錄用戶的UID與SessionId對於的數據
 26  *
 27  * 3.VID_PREFIX開頭,存放位於指定頁面用戶的數據(與Ajax一起使用,用於實現指定頁面同時瀏覽人數的限制功能)
 28  * 
 29  * @ClassName: OnlineUtils
 30  * @Description: 在線列表操作工具類
 31  * @author BuilderQiu
 32  * @date 2014-1-9 上午09:25:43
 33  *
 34  */
 35 public class OnlineUtils {
 36     
 37     //KEY值根據SessionID生成    
 38     private static final String SID_PREFIX = "online:sid:";
 39     private static final String UID_PREFIX = "online:uid:";
 40     private static final String VID_PREFIX = "online:vid:";
 41     private static final int OVERDATETIME = 30 * 60;
 42     private static final int BROADCAST_OVERDATETIME = 70;//Ajax每60秒發起一次,超過BROADCAST_OVERDATETIME時間長度未發起表示已經離開該頁面
 43 
 44     public static void login(String sid,ClientUser user){
 45         
 46         Jedis jedis = RedisPoolUtils.getJedis();
 47 
 48         jedis.setex(SID_PREFIX+sid, OVERDATETIME, userToString(user));
 49         jedis.setex(UID_PREFIX+user.getId(), OVERDATETIME, sid);
 50         
 51         RedisPoolUtils.release(jedis);
 52     }
 53     
 54     public static void broadcast(String uid,String identify){
 55         
 56         if(uid==null||"".equals(uid)) //異常數據,正常情況下登陸用戶才會發起該請求
 57             return ;
 58         
 59         Jedis jedis = RedisPoolUtils.getJedis();
 60         
 61         jedis.setex(VID_PREFIX+identify+":"+uid, BROADCAST_OVERDATETIME, uid);
 62         
 63         RedisPoolUtils.release(jedis);
 64     }
 65     
 66     
 67     private static String userToString(ClientUser user){
 68         JsonConfig  config = new JsonConfig();
 69         JsonValueProcessor processor = new JsonDateValueProcessor("yyyy-MM-dd HH:mm:ss");
 70         config.registerJsonValueProcessor(Date.class, processor);
 71         JSONObject obj = JSONObject.fromObject(user, config);
 72 
 73         return obj.toString();
 74     }
 75     
 76     /**
 77      * 
 78      * @Title: logout
 79      * @Description: 退出
 80      * @param @param sessionId
 81      * @return void
 82      * @throws
 83      */
 84     public static void logout(String sid,String uid){
 85         
 86         Jedis jedis = RedisPoolUtils.getJedis();
 87         
 88         jedis.del(SID_PREFIX+sid);
 89         jedis.del(UID_PREFIX+uid);
 90         
 91         RedisPoolUtils.release(jedis);
 92     }
 93     
 94     /**
 95      * 
 96      * @Title: logout
 97      * @Description: 退出
 98      * @param @param UserId  使指定用戶下線
 99      * @return void
100      * @throws
101      */
102     public static void logout(String uid){
103         Jedis jedis = RedisPoolUtils.getJedis();
104         
105         //刪除sid
106         jedis.del(SID_PREFIX+jedis.get(UID_PREFIX+uid));
107         //刪除uid
108         jedis.del(UID_PREFIX+uid);
109         
110         RedisPoolUtils.release(jedis);
111     }
112     
113     public static String getClientUserBySessionId(String sid){
114         
115         Jedis jedis = RedisPoolUtils.getJedis();
116         
117         String user = jedis.get(SID_PREFIX+sid);
118         
119         RedisPoolUtils.release(jedis);
120         
121         return user;
122     }
123     
124     public static String getClientUserByUid(String uid){
125         Jedis jedis = RedisPoolUtils.getJedis();
126         
127         String user = jedis.get(SID_PREFIX+jedis.get(UID_PREFIX+uid));
128         
129         RedisPoolUtils.release(jedis);
130         
131         return user;
132     }
133     
134     /**
135      * 
136      * @Title: online
137      * @Description: 所有的key
138      * @return List  
139      * @throws
140      */
141     public static List online(){
142 
143         Jedis jedis = RedisPoolUtils.getJedis();
144         
145         Set online = jedis.keys(SID_PREFIX+"*");
146         
147         RedisPoolUtils.release(jedis);
148         return new ArrayList(online);
149     }
150     
151     /**
152      * 
153      * @Title: online
154      * @Description: 分頁顯示在線列表
155      * @return List  
156      * @throws
157      */
158     public static List onlineByPage(int page,int pageSize) throws Exception{
159         
160         Jedis jedis = RedisPoolUtils.getJedis();
161         
162         Set onlineSet = jedis.keys(SID_PREFIX+"*");
163         
164         
165         List onlines =new ArrayList(onlineSet);
166         
167         if(onlines.size() == 0){
168             return null;
169         }
170         
171         Pipeline pip = jedis.pipelined();
172         for(Object key:onlines){
173             pip.get(getKey(key));
174         }
175         List result = pip.syncAndReturnAll();
176         RedisPoolUtils.release(jedis);
177         
178         List<ClientUser> listUser=new ArrayList<ClientUser>();
179         for(int i=0;i<result.size();i++){
180             listUser.add(Constants.json2ClientUser((String)result.get(i)));
181         }
182         Collections.sort(listUser,new Comparator<ClientUser>(){
183             public int compare(ClientUser o1, ClientUser o2) {
184                 return o2.getLastLoginTime().compareTo(o1.getLastLoginTime());
185             }
186         });
187         onlines=listUser;
188         int start = (page - 1) * pageSize;
189         int toIndex=(start+pageSize)>onlines.size()?onlines.size():start+pageSize;
190         List list = onlines.subList(start, toIndex);
191     
192         return list;
193     }
194     
195     private static String getKey(Object obj){
196         
197         String temp = String.valueOf(obj);
198         String key[] = temp.split(":");
199 
200         return SID_PREFIX+key[key.length-1];
201     }
202     
203     /**
204      * 
205      * @Title: onlineCount
206      * @Description: 總在線人數
207      * @param @return
208      * @return int
209      * @throws
210      */
211     public static int onlineCount(){
212         
213         Jedis jedis = RedisPoolUtils.getJedis();
214         
215         Set online = jedis.keys(SID_PREFIX+"*");
216         
217         RedisPoolUtils.release(jedis);
218         
219         return online.size();
220         
221     }
222     
223     /**
224      * 獲取指定頁面在線人數總數
225      */
226     public static int broadcastCount(String identify) {
227         Jedis jedis = RedisPoolUtils.getJedis();
228         
229         Set online = jedis.keys(VID_PREFIX+identify+":*");
230         
231         
232 
233         RedisPoolUtils.release(jedis);
234         
235         return online.size();
236     }
237     
238     /**
239      * 自己是否在線
240      */
241     public static boolean broadcastIsOnline(String identify,String uid) {
242         
243         Jedis jedis = RedisPoolUtils.getJedis();
244         
245         String online = jedis.get(VID_PREFIX+identify+":"+uid);
246         
247         RedisPoolUtils.release(jedis);
248         
249         return !StringUtil.isBlank(online);//不為空就代表已經找到數據了,也就是上線了
250     }
251     
252     /**
253      * 獲取指定頁面在線人數總數
254      */
255     public static int broadcastCount() {
256         Jedis jedis = RedisPoolUtils.getJedis();
257         
258         Set online = jedis.keys(VID_PREFIX+"*");
259         
260         RedisPoolUtils.release(jedis);
261         
262         return online.size();
263     }
264     
265     
266     /**
267      * 
268      * @Title: isOnline
269      * @Description: 指定賬號是否登陸
270      * @param @param sessionId
271      * @param @return
272      * @return boolean 
273      * @throws
274      */
275     public static boolean isOnline(String uid){
276         
277         Jedis jedis = RedisPoolUtils.getJedis();
278         
279         boolean isLogin = jedis.exists(UID_PREFIX+uid);
280         
281         RedisPoolUtils.release(jedis);
282         
283         return isLogin;
284     }
285     
286     public static boolean isOnline(String uid,String sid){
287         
288         Jedis jedis = RedisPoolUtils.getJedis();
289         
290         String loginSid = jedis.get(UID_PREFIX+uid);
291         
292         RedisPoolUtils.release(jedis);
293         
294         return sid.equals(loginSid);
295     }
296 }

    由於在線狀態是記錄在Redis中的,並不單純依靠Session的過期機制來實現,所以需要通過攔截器在每次發送請求的時候去更新Redis中相應的緩存過期時間來更新用戶的在線狀態。

  登陸、退出操作與單機版相似,強制下線需要配合攔截器實現,當用戶下次訪問的時候,自己來校驗自己的狀態是否為已經下線,不再由服務器控制。

  配合攔截器實現在線狀態維持與強制登陸(使其他地方登陸了該賬戶的用戶下線)功能:

 1 ...
 2 if(uid != null){//已登錄
 3     if(!OnlineUtils.isOnline(uid, session.getId())){
 4         session.invalidate();
 5 
 6         return ai.invoke();
 7     }else{
 8         OnlineUtils.login(session.getId(), (ClientUser)session.getAttribute("clientUser"));
 9         //刷新緩存
10     }
11 }
12 ...

注:Redis在線列表工具類中的部分代碼是后來需要實現限制同時訪問指定頁面瀏覽人數功能而添加的,同樣基於Redis實現,前端由Ajax輪詢來更新用戶停留頁面的狀態。

附錄:

  Redis連接池配置文件:

 1 ###redis##config########
 2 #redis服務器ip # 
 3 #redis.ip=localhost
 4 #redis服務器端口號#
 5 redis.port=6379
 6 
 7 ###jedis##pool##config###
 8 #jedis的最大分配對象#
 9 jedis.pool.maxActive=1024
10 #jedis最大保存idel狀態對象數 #
11 jedis.pool.maxIdle=200
12 #jedis池沒有對象返回時,最大等待時間 #
13 jedis.pool.maxWait=1000
14 #jedis調用borrowObject方法時,是否進行有效檢查#
15 jedis.pool.testOnBorrow=true
16 #jedis調用returnObject方法時,是否進行有效檢查 #
17 jedis.pool.testOnReturn=true

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM