前言:

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

在单机环境下,在线列表的实现方案可以采用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. git(工作区,暂存区,管理修改,撤销修改,删除文件)

    工作区和暂存区 984次阅读 Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念. 先来看名词解释. 工作区(Working Directory) 就是你在电脑里能看到的目录,比如我的l ...

  2. 交互式shell和非交互式shell的区别

    交互式模式就是shell等待你的输入,并且执行你提交的命令.这种模式被称作交互式是因为shell与用户进行交互.这种模式也是大多数用户非常熟悉的:登录.执行一些命令.签退.当你签退后,shell也终止 ...

  3. Storm官方帮助手册翻译(下)

    使用其他语言编写Bolt Bolt可以使用任意语言编写.用另外一种语言编写Bolt来作为子进程运行.Storm会在标准输入输出的基础上使用Json来与子进程通信.通信协议之需要一个100行的适配器库, ...

  4. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;方法意思

    这个方法是用来设置你的TableView中每一行显示的内容和格式的. indexPath 用来指示当前单元格,它的row方法可以获得这个单元格的行号,section方法可以获得这个单元格所处的区域号 ...

  5. 笨方法学python--变量和命名

    1 =(单等号)和==(双等号)的区别 =用来赋值, ==用来判断是否相等 2 x = 100 在操作符2侧加空格,易读 3 打印时,进行字符串拼接 print "there are&quo ...

  6. 转: Windows如何打开和使用事件查看器管理计算机

    方法/步骤   1 右键单击"我的电脑"(win8中名称为"这台电脑.This Computer"),选择"管理",点击. 步骤阅读 2 出 ...

  7. php秒杀

    我们知道数据库处理sql是一条条处理的,假设购买商品的流程是这样的: sql1:查询商品库存 ? 1 2 3 4 5 if(库存数量 > 0) {   //生成订单...   sql2:库存-1 ...

  8. javaweb 国际化

    国际化又称为 i18n:internationalization 软件实现国际化,需具备哪些特征:对于程序中固定使用的文本元素,例如菜单栏.导航条等中使用的文本元素.或错误提示信息,状态信息等,需要根 ...

  9. boost库之geometry

    环境:win732位旗舰版.VS2010旗舰版.boost 1.55.0版本.坐标系为MM_TEXT Geometry是一个开源的几何计算库,包含了几何图形最基本的操作(也支持复杂的操作),下面我们看 ...

  10. 函数求值一<找规律>

    函数求值 题意: 定义函数g(n)为n最大的奇数因子.求f(n)=g(1)+g(2)+g(3)+-+g(n).1<=n<=10^8; 思路: 首先明白暴力没法过.问题是如何求解,二分.知道 ...