前言:
由于項(xiàng)目需求,需要在集群環(huán)境下實(shí)現(xiàn)在線用戶列表的功能,并依靠在線列表實(shí)現(xiàn)用戶單一登陸(同一賬戶只能一處登陸)功能:
在單機(jī)環(huán)境下,在線列表的實(shí)現(xiàn)方案可以采用SessionListener來完成,當(dāng)有Session創(chuàng)建和銷毀的時(shí)候做相應(yīng)的操作即可完成功能及將相應(yīng)的Session的引用存放于內(nèi)存中,由于持有了所有的Session的引用,故可以方便的實(shí)現(xiàn)用戶單一登陸的功能(比如在第二次登陸的時(shí)候使之前登陸的賬戶所在的Session失效)。
而在集群環(huán)境下,由于用戶的請(qǐng)求可能分布在不同的Web服務(wù)器上,繼續(xù)將在線用戶列表儲(chǔ)存在單機(jī)內(nèi)存中已經(jīng)不能滿足需要,不同的Web服務(wù)器將會(huì)產(chǎn)生不同的在線列表,并且不能有效的實(shí)現(xiàn)單一用戶登陸的功能,因?yàn)槟骋挥脩艨赡懿⒉辉诮邮艿酵顺稣?qǐng)求的Web服務(wù)器的在線用戶列表中(在集群中的某臺(tái)服務(wù)器上完成的登陸操作,而在其他服務(wù)器上完成退出操作)。
現(xiàn)有解決方案:
1.將用戶的在線情況記錄進(jìn)入數(shù)據(jù)庫中,依靠數(shù)據(jù)庫完成對(duì)登陸狀況的檢測(cè)
2.將在線列表放在一個(gè)公共的緩存服務(wù)器上
由于緩存服務(wù)器可以為緩存內(nèi)容設(shè)置指定有效期,可以方便實(shí)現(xiàn)Session過期的效果,以及避免讓數(shù)據(jù)庫的讀寫性能成為系統(tǒng)瓶頸等原因,我們采用了Redis來作為緩存服務(wù)器用于實(shí)現(xiàn)該功能。
單機(jī)環(huán)境下的解決方案:
基于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信息,為在線列表做基礎(chǔ)
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");
//
已登陸狀態(tài)會(huì)將用戶的UserId保存在session中
30
if
(!StringUtil.isBlank(uid)){
//
判斷是否登陸狀態(tài)
31
removeSession(uid);
32
}
33
}
34
public
static
void
removeSession(String uid){
35
HttpSession session =
sessionList.get(uid);
36
try
{
37
sessionList.remove(uid);
//
先執(zhí)行,防止session.invalidate()報(bào)錯(cuò)而不執(zhí)行
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: 檢測(cè)是否已經(jīng)登陸
64
*
@param
@param
uid 用戶UserId
65
*
@param
@param
sid 發(fā)起請(qǐng)求的用戶的SessionId
66
*
@return
boolean true 校驗(yàn)通過
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
}
用戶的在線狀態(tài)全部維護(hù)記錄在sessionList中,并且可以通過sessionList獲取到任意用戶的session對(duì)象,可以用來完成使指定用戶離線的功能(調(diào)用該用戶的session.invalidate()方法)。
用戶登錄的時(shí)候調(diào)用addSession(uid,session)方法將用戶與其登錄的Session信息記錄至sessionList中,再退出的時(shí)候調(diào)用removeSession(session) or removeSession(uid)方法,在強(qiáng)制下線的時(shí)候調(diào)用removeSession(uid)方法,以及一些其他的操作即可實(shí)現(xiàn)相應(yīng)的功能。
基于Redis的解決方案:
該解決方案的實(shí)質(zhì)是將在線列表的所在的內(nèi)存共享出來,讓集群環(huán)境下所有的服務(wù)器都能夠訪問到這部分?jǐn)?shù)據(jù),并且將用戶的在線狀態(tài)在這塊內(nèi)存中進(jìn)行維護(hù)。
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
//
設(shè)置池配置項(xiàng)值
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數(shù)據(jù)
56
* 2.UID_PREFIX開頭,存放登錄用戶的UID與SessionId對(duì)于的數(shù)據(jù)
57
*
58
* 3.VID_PREFIX開頭,存放位于指定頁面用戶的數(shù)據(jù)(與Ajax一起使用,用于實(shí)現(xiàn)指定頁面同時(shí)瀏覽人數(shù)的限制功能)
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值根據(jù)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秒發(fā)起一次,超過BROADCAST_OVERDATETIME時(shí)間長度未發(fā)起表示已經(jīng)離開該頁面
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))
//
異常數(shù)據(jù),正常情況下登陸用戶才會(huì)發(fā)起該請(qǐng)求
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: 總在線人數(shù)
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
* 獲取指定頁面在線人數(shù)總數(shù)
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);
//
不為空就代表已經(jīng)找到數(shù)據(jù)了,也就是上線了
219
}
220
/**
221
* 獲取指定頁面在線人數(shù)總數(shù)
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: 指定賬號(hào)是否登陸
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
}
由于在線狀態(tài)是記錄在Redis中的,并不單純依靠Session的過期機(jī)制來實(shí)現(xiàn),所以需要通過攔截器在每次發(fā)送請(qǐng)求的時(shí)候去更新Redis中相應(yīng)的緩存過期時(shí)間來更新用戶的在線狀態(tài)。
登陸、退出操作與單機(jī)版相似,強(qiáng)制下線需要配合攔截器實(shí)現(xiàn),當(dāng)用戶下次訪問的時(shí)候,自己來校驗(yàn)自己的狀態(tài)是否為已經(jīng)下線,不再由服務(wù)器控制。
配合攔截器實(shí)現(xiàn)在線狀態(tài)維持與強(qiáng)制登陸(使其他地方登陸了該賬戶的用戶下線)功能:
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在線列表工具類中的部分代碼是后來需要實(shí)現(xiàn)限制同時(shí)訪問指定頁面瀏覽人數(shù)功能而添加的,同樣基于Redis實(shí)現(xiàn),前端由Ajax輪詢來更新用戶停留頁面的狀態(tài)。
附錄:
Redis連接池配置文件:
###redis##config########
#redis服務(wù)器ip #
#redis.ip=
localhost
#redis服務(wù)器端口號(hào)#
redis
.port=6379
###jedis##pool##config###
#jedis的最大分配對(duì)象#
jedis
.pool.maxActive=1024
#jedis最大保存idel狀態(tài)對(duì)象數(shù) #
jedis
.pool.maxIdle=200
#jedis池沒有對(duì)象返回時(shí),最大等待時(shí)間 #
jedis
.pool.
maxWait
=1000
#jedis調(diào)用borrowObject方法時(shí),是否進(jìn)行有效檢查#
jedis
.pool.testOnBorrow=
true
#jedis調(diào)用returnObject方法時(shí),是否進(jìn)行有效檢查 #
jedis
.pool.testOnReturn=true
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元

