前言:
由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能:
在单机环境下,在线列表的实现方案可以采用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

