前言:

由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能:

在单机环境下,在线列表的实现方案可以采用SessionListener来完成,当有Session创建和销毁的时候做相应的操作即可完成功能及将相应的Session的引用存放于内存中,由于持有了所有的Session的引用,故可以方便的实现用户单一登陆的功能(比如在第二次登陆的时候使之前登陆的账户所在的Session失效)。

而在集群环境下,由于用户的请求可能分布在不同的Web服务器上,继续将在线用户列表储存在单机内存中已经不能满足需要,不同的Web服务器将会产生不同的在线列表,并且不能有效的实现单一用户登陆的功能,因为某一用户可能并不在接受到退出请求的Web服务器的在线用户列表中(在集群中的某台服务器上完成的登陆操作,而在其他服务器上完成退出操作)。

现有解决方案:

1.将用户的在线情况记录进入数据库中,依靠数据库完成对登陆状况的检测

2.将在线列表放在一个公共的缓存服务器上

由于缓存服务器可以为缓存内容设置指定有效期,可以方便实现Session过期的效果,以及避免让数据库的读写性能成为系统瓶颈等原因,我们采用了Redis来作为缓存服务器用于实现该功能。

单机环境下的解决方案:

基于HttpSessionListener:

 import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import com.xxx.common.util.StringUtil;
/**
*
* @ClassName: SessionListener
* @Description: 记录所有登陆的Session信息,为在线列表做基础
* @author libaoting
* @date 2013-9-18 09:35:13
*
*/
public class SessionListener implements HttpSessionListener {
//在线列表<uid,session>
private static Hashtable<String,HttpSession> sessionList = new Hashtable<String, HttpSession>();
public void sessionCreated(HttpSessionEvent event) {
//不做处理,只处理登陆用户的列表
}
public void sessionDestroyed(HttpSessionEvent event) {
removeSession(event.getSession());
}
public static void removeSession(HttpSession session){
if(session == null){
return ;
}
String uid=(String)session.getAttribute("clientUserId");//已登陆状态会将用户的UserId保存在session中
if(!StringUtil.isBlank(uid)){//判断是否登陆状态
removeSession(uid);
}
}
public static void removeSession(String uid){
HttpSession session = sessionList.get(uid);
try{
sessionList.remove(uid);//先执行,防止session.invalidate()报错而不执行
if(session != null){
session.invalidate();
}
}catch (Exception e) {
System.out.println("Session invalidate error!");
}
}
public static void addSession(String uid,HttpSession session){
sessionList.put(uid, session);
}
public static int getSessionCount(){
return sessionList.size();
}
public static Iterator<HttpSession> getSessionSet(){
return sessionList.values().iterator();
}
public static HttpSession getSession(String id){
return sessionList.get(id);
}
public static boolean contains(String uid){
return sessionList.containsKey(uid);
}
/**
*
* @Title: isLoginOnThisSession
* @Description: 检测是否已经登陆
* @param @param uid 用户UserId
* @param @param sid 发起请求的用户的SessionId
* @return boolean true 校验通过
*/
public static boolean isLoginOnThisSession(String uid,String sid){
if(uid==null||sid==null){
return false;
}
if(contains(uid)){
HttpSession session = sessionList.get(uid);
if(session!=null&&session.getId().equals(sid)){
return true;
}
}
return false;
}
}

  用户的在线状态全部维护记录在sessionList中,并且可以通过sessionList获取到任意用户的session对象,可以用来完成使指定用户离线的功能(调用该用户的session.invalidate()方法)。

用户登录的时候调用addSession(uid,session)方法将用户与其登录的Session信息记录至sessionList中,再退出的时候调用removeSession(session) or removeSession(uid)方法,在强制下线的时候调用removeSession(uid)方法,以及一些其他的操作即可实现相应的功能。

基于Redis的解决方案:

该解决方案的实质是将在线列表的所在的内存共享出来,让集群环境下所有的服务器都能够访问到这部分数据,并且将用户的在线状态在这块内存中进行维护。

Redis连接池工具类:

 import java.util.ResourceBundle;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisPoolUtils {
private static final JedisPool pool;
static{
ResourceBundle bundle = ResourceBundle.getBundle("redis");
JedisPoolConfig config = new JedisPoolConfig();
if (bundle == null) {
throw new IllegalArgumentException("[redis.properties] is not found!");
}
//设置池配置项值
config.setMaxActive(Integer.valueOf(bundle.getString("jedis.pool.maxActive")));
config.setMaxIdle(Integer.valueOf(bundle.getString("jedis.pool.maxIdle")));
config.setMaxWait(Long.valueOf(bundle.getString("jedis.pool.maxWait")));
config.setTestOnBorrow(Boolean.valueOf(bundle.getString("jedis.pool.testOnBorrow")));
config.setTestOnReturn(Boolean.valueOf(bundle.getString("jedis.pool.testOnReturn")));
pool = new JedisPool(config, bundle.getString("redis.ip"),Integer.valueOf(bundle.getString("redis.port")) );
}
/**
*
* @Title: release
* @Description: 释放连接
* @param @param jedis
* @return void
* @throws
*/
public static void release(Jedis jedis){
pool.returnResource(jedis);
}
public static Jedis getJedis(){
return pool.getResource();
}
}
Redis在线列表工具类:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Set;
import net.sf.json.JSONObject;
import net.sf.json.JsonConfig;
import net.sf.json.processors.JsonValueProcessor;
import cn.sccl.common.util.StringUtil;
import com.xxx.common.util.JsonDateValueProcessor;
import com.xxx.user.model.ClientUser;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import tools.Constants;
/**
*
* Redis缓存中存放两组key:
* 1.SID_PREFIX开头,存放登陆用户的SessionId与ClientUser的Json数据
* 2.UID_PREFIX开头,存放登录用户的UID与SessionId对于的数据
*
* 3.VID_PREFIX开头,存放位于指定页面用户的数据(与Ajax一起使用,用于实现指定页面同时浏览人数的限制功能)
*
* @ClassName: OnlineUtils
* @Description: 在线列表操作工具类
* @author BuilderQiu
* @date 2014-1-9 上午09:25:43
*
*/
public class OnlineUtils {
//KEY值根据SessionID生成
private static final String SID_PREFIX = "online:sid:";
private static final String UID_PREFIX = "online:uid:";
private static final String VID_PREFIX = "online:vid:";
private static final int OVERDATETIME = 30 * 60;
private static final int BROADCAST_OVERDATETIME = 70;//ax每60秒发起一次,超过BROADCAST_OVERDATETIME时间长度未发起表示已经离开该页面
public static void login(String sid,ClientUser user){
Jedis jedis = RedisPoolUtils.getJedis();
jedis.setex(SID_PREFIX+sid, OVERDATETIME, userToString(user));
jedis.setex(UID_PREFIX+user.getId(), OVERDATETIME, sid);
RedisPoolUtils.release(jedis);
}
public static void broadcast(String uid,String identify){
if(uid==null||"".equals(uid)) //异常数据,正常情况下登陆用户才会发起该请求
return ;
Jedis jedis = RedisPoolUtils.getJedis();
jedis.setex(VID_PREFIX+identify+":"+uid, BROADCAST_OVERDATETIME, uid);
RedisPoolUtils.release(jedis);
}
private static String userToString(ClientUser user){
JsonConfig config = new JsonConfig();
JsonValueProcessor processor = new JsonDateValueProcessor("yyyy-MM-dd HH:mm:ss");
config.registerJsonValueProcessor(Date.class, processor);
JSONObject obj = JSONObject.fromObject(user, config);
return obj.toString();
}
/**
*
* @Title: logout
* @Description: 退出
* @param @param sessionId
* @return void
* @throws
*/
public static void logout(String sid,String uid){
Jedis jedis = RedisPoolUtils.getJedis();
jedis.del(SID_PREFIX+sid);
jedis.del(UID_PREFIX+uid);
RedisPoolUtils.release(jedis);
}
/**
*
* @Title: logout
* @Description: 退出
* @param @param UserId 使指定用户下线
* @return void
* @throws
*/
public static void logout(String uid){
Jedis jedis = RedisPoolUtils.getJedis();
//删除sid
jedis.del(SID_PREFIX+jedis.get(UID_PREFIX+uid));
//删除uid
jedis.del(UID_PREFIX+uid);
RedisPoolUtils.release(jedis);
}
public static String getClientUserBySessionId(String sid){
Jedis jedis = RedisPoolUtils.getJedis();
String user = jedis.get(SID_PREFIX+sid);
RedisPoolUtils.release(jedis);
return user;
}
public static String getClientUserByUid(String uid){
Jedis jedis = RedisPoolUtils.getJedis();
String user = jedis.get(SID_PREFIX+jedis.get(UID_PREFIX+uid));
RedisPoolUtils.release(jedis);
return user;
}
/**
*
* @Title: online
* @Description: 所有的key
* @return List
* @throws
*/
public static List online(){
Jedis jedis = RedisPoolUtils.getJedis();
Set online = jedis.keys(SID_PREFIX+"*");
RedisPoolUtils.release(jedis);
return new ArrayList(online);
}
/**
*
* @Title: online
* @Description: 分页显示在线列表
* @return List
* @throws
*/
public static List onlineByPage(int page,int pageSize) throws Exception{
Jedis jedis = RedisPoolUtils.getJedis();
Set onlineSet = jedis.keys(SID_PREFIX+"*");
List onlines =new ArrayList(onlineSet);
if(onlines.size() == 0){
return null;
}
Pipeline pip = jedis.pipelined();
for(Object key:onlines){
pip.get(getKey(key));
}
List result = pip.syncAndReturnAll();
RedisPoolUtils.release(jedis);
List<ClientUser> listUser=new ArrayList<ClientUser>();
for(int i=0;i<result.size();i++){
listUser.add(Constants.json2ClientUser((String)result.get(i)));
}
Collections.sort(listUser,new Comparator<ClientUser>(){
public int compare(ClientUser o1, ClientUser o2) {
return o2.getLastLoginTime().compareTo(o1.getLastLoginTime());
}
});
onlines=listUser;
int start = (page - 1) * pageSize;
int toIndex=(start+pageSize)>onlines.size()?onlines.size():start+pageSize;
List list = onlines.subList(start, toIndex);
return list;
}
private static String getKey(Object obj){
String temp = String.valueOf(obj);
String key[] = temp.split(":");
return SID_PREFIX+key[key.length-1];
}
/**
*
* @Title: onlineCount
* @Description: 总在线人数
* @param @return
* @return int
* @throws
*/
public static int onlineCount(){
Jedis jedis = RedisPoolUtils.getJedis();
Set online = jedis.keys(SID_PREFIX+"*");
RedisPoolUtils.release(jedis);
return online.size();
}
/**
* 获取指定页面在线人数总数
*/
public static int broadcastCount(String identify) {
Jedis jedis = RedisPoolUtils.getJedis();
Set online = jedis.keys(VID_PREFIX+identify+":*");
RedisPoolUtils.release(jedis);
return online.size();
}
/**
* 自己是否在线
*/
public static boolean broadcastIsOnline(String identify,String uid) {
Jedis jedis = RedisPoolUtils.getJedis();
String online = jedis.get(VID_PREFIX+identify+":"+uid);
RedisPoolUtils.release(jedis);
return !StringUtil.isBlank(online);//不为空就代表已经找到数据了,也就是上线了
}
/**
* 获取指定页面在线人数总数
*/
public static int broadcastCount() {
Jedis jedis = RedisPoolUtils.getJedis();
Set online = jedis.keys(VID_PREFIX+"*");
RedisPoolUtils.release(jedis);
return online.size();
}
/**
*
* @Title: isOnline
* @Description: 指定账号是否登陆
* @param @param sessionId
* @param @return
* @return boolean
* @throws
*/
public static boolean isOnline(String uid){
Jedis jedis = RedisPoolUtils.getJedis();
boolean isLogin = jedis.exists(UID_PREFIX+uid);
RedisPoolUtils.release(jedis);
return isLogin;
}
public static boolean isOnline(String uid,String sid){
Jedis jedis = RedisPoolUtils.getJedis();
String loginSid = jedis.get(UID_PREFIX+uid);
RedisPoolUtils.release(jedis);
return sid.equals(loginSid);
}
}

  由于在线状态是记录在Redis中的,并不单纯依靠Session的过期机制来实现,所以需要通过拦截器在每次发送请求的时候去更新Redis中相应的缓存过期时间来更新用户的在线状态。

登陆、退出操作与单机版相似,强制下线需要配合拦截器实现,当用户下次访问的时候,自己来校验自己的状态是否为已经下线,不再由服务器控制。

配合拦截器实现在线状态维持与强制登陆(使其他地方登陆了该账户的用户下线)功能:

   ...
if(uid != null){//已登录
if(!OnlineUtils.isOnline(uid, session.getId())){
session.invalidate();
return ai.invoke();
}else{
OnlineUtils.login(session.getId(), (ClientUser)session.getAttribute("clientUser"));
//刷新缓存
}
}
...

注: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

基于Redis的在线用户列表解决方案的更多相关文章

  1. [项目回顾]基于Redis的在线用户列表解决方案

    迁移:基于Redis的在线用户列表解决方案 前言: 由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能: 在单机环境下,在线列表的实现方案 ...

  2. 使用 Redis 统计在线用户人数

    在构建应用的时候, 我们经常需要对用户的一举一动进行记录, 而其中一个比较重要的操作, 就是对在线的用户进行记录. 本文将介绍四种使用 Redis 对在线用户进行记录的方案, 这些方案虽然都可以对在线 ...

  3. 基于Redis位图实现用户签到功能

    场景需求 适用场景如签到送积分.签到领取奖励等,大致需求如下: 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等. 如果连续签到中断,则重置计数,每月初重置计数. 当月签到满 ...

  4. DIOCP之获取在线用户列表

    通过获取tcpserver.getonlinecontextlist来得到在线列表 procedure TfrmMain.btn_refreshClick(Sender: TObject);var l ...

  5. SharedObject使用:在FluorineFx.net与Flex中使用共享对象维护在线用户列表实例【转】

    一.添加一个新的FluorineFx的服务类项目OnLineService,删除原有的Sample.cs,并添加一个用户类定义与一个ApplicationAdpater类:如下: /*-- User. ...

  6. Redis位图实现用户签到功能

    场景需求 适用场景如签到送积分.签到领取奖励等,大致需求如下: 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等. 如果连续签到中断,则重置计数,每月初重置计数. 当月签到满 ...

  7. Javaweb基础--->利用监听器统计在线用户数量和用户信息

    首页布局:index.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" ...

  8. Servlet监听器及在线用户

    Servlet中的监听器分为三种类型Ⅰ 监听ServletContext.Request.Session作用域的创建和销毁 (1)ServletContextListener (2)HttpSessi ...

  9. .NET基于Redis缓存实现单点登录SSO的解决方案[转]

    一.基本概念 最近公司的多个业务系统要统一整合使用同一个登录,这就是我们耳熟能详的单点登录,现在就NET基于Redis缓存实现单点登录做一个简单的分享. 单点登录(Single Sign On),简称 ...

随机推荐

  1. nfs服务器的建立

    NFS服务器的配置 一.NFS服务器端的配置,即共享发布者 (一)需启动的服务和需安装的软件 1.NFS服务器必须启动两个daemons服务:rpc.nfsd和rpc.mountd   rpc.nfs ...

  2. linux中的 tar命令的 -C 参数,以及其它一些参数(转)

    linux中的 tar命令的 -C 参数,以及其它一些参数 复制源:http://www.cnblogs.com/li-hao/archive/2011/10/03/2198480.htmltar命令 ...

  3. json 数组 对象 xml 之间转换(待补充)

    json 数组  xml 对象   之间转换(待补充) 1 把对象的类型或者数组转换成字符串类型(或者更确切的说是json类型的). 此处参考链接http://www.jb51.net/article ...

  4. Educational Codeforces Round 15_D. Road to Post Office

    D. Road to Post Office time limit per test 1 second memory limit per test 256 megabytes input standa ...

  5. luci 随笔

    entry()函数, 第一个参数是定义菜单的显示(Virtual path). 第二个参数定义相应的处理方式(target). alias是指向别的entry的别名,from调用的某一个view,cb ...

  6. LeetCode OJ 109. Convert Sorted List to Binary Search Tree

    Given a singly linked list where elements are sorted in ascending order, convert it to a height bala ...

  7. Linux Ubuntu 内核升级

    方法一 : 1 更新系统源 apt-get update 2 搜索内核文件 apt-cache search linux-image 3 安装 apt-get install -y  linux-im ...

  8. linux fork()函数

    C语言编程创建函数fork() 执行解析 | 浏览:1842 | 更新:2013-04-22 15:12 | 标签:c语言 概述 最近在看进程间的通信,看到了fork()函数,虽然以前用过,这次经过思 ...

  9. String.Format(string, arg0)中sring格式

    复合格式字符串和对象列表将用作支持复合格式设置功能的方法的参数.复合格式字符串由零个或多个固定文本段与一个或多个格式项混和组成.固定文本是所选择的任何字符串,并且每个格式项对应于列表中的一个对象或装箱 ...

  10. 获取Excel部分数据并很据项目要求计算适宜性等级综合指数判断该地区的土壤适宜性

    代码运行前请先导入jxl架包,以下代码仅供学习参考: 下图为项目中的Excel: ExcelTest02类代码如下: // 读取Excel的类 import java.io.BufferedWrite ...