前言:
由于項目需求,需要在集群環境下實現在線用戶列表的功能,并依靠在線列表實現用戶單一登陸(同一賬戶只能一處登陸)功能:
在單機環境下,在線列表的實現方案可以采用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元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

