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

来源:互联网 发布:淘宝更换宝贝详情页 编辑:程序博客网 时间:2024/05/17 09:01

前言:

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

  在单机环境下,在线列表的实现方案可以采用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  5 import javax.servlet.http.HttpSession; 6 import javax.servlet.http.HttpSessionEvent; 7 import javax.servlet.http.HttpSessionListener; 8  9 import com.xxx.common.util.StringUtil;10 11 /**12  * 13  * @ClassName: SessionListener14  * @Description: 记录所有登陆的Session信息,为在线列表做基础15  * @author BuilderQiu16  * @date 2013-9-18 09:35:1317  *18  */19 public class SessionListener implements HttpSessionListener {20     21     //在线列表<uid,session>22     private static Hashtable<String,HttpSession> sessionList = new Hashtable<String, HttpSession>();23     24 25     public void sessionCreated(HttpSessionEvent event) {26         //不做处理,只处理登陆用户的列表27         28     }29 30     public void sessionDestroyed(HttpSessionEvent event) {31         removeSession(event.getSession());32     }33     34     public static void removeSession(HttpSession session){35         if(session == null){36             return ;37         }38 39         String uid=(String)session.getAttribute("clientUserId");//已登陆状态会将用户的UserId保存在session中40         if(!StringUtil.isBlank(uid)){//判断是否登陆状态41             removeSession(uid);42         }43     }44     45     public static void removeSession(String uid){46         HttpSession session = sessionList.get(uid);47         try{48             sessionList.remove(uid);//先执行,防止session.invalidate()报错而不执行49             if(session != null){50                 session.invalidate();51             }52         }catch (Exception e) {53             System.out.println("Session invalidate error!");54         }55     }56     57     public static void addSession(String uid,HttpSession session){58         sessionList.put(uid, session);59     }60     61     public static int getSessionCount(){62         return sessionList.size();63     }64     65     public static Iterator<HttpSession> getSessionSet(){66         return sessionList.values().iterator();67     }68     69     public static HttpSession getSession(String id){70         return sessionList.get(id);71     }72     73     public static boolean contains(String uid){74         return sessionList.containsKey(uid);75     }76     77     /**78      * 79      * @Title: isLoginOnThisSession80      * @Description: 检测是否已经登陆81      * @param @param uid 用户UserId82      * @param @param sid 发起请求的用户的SessionId83      * @return boolean true 校验通过 84      */85     public static boolean isLoginOnThisSession(String uid,String sid){86         if(uid==null||sid==null){87             return false;88         }89         if(contains(uid)){90             HttpSession session = sessionList.get(uid);91             92             if(session!=null&&session.getId().equals(sid)){93                 return true;94             }95         }96         return false;97     }98     99 }
复制代码

  用户的在线状态全部维护记录在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  3 import redis.clients.jedis.Jedis; 4 import redis.clients.jedis.JedisPool; 5 import redis.clients.jedis.JedisPoolConfig; 6  7 public class RedisPoolUtils { 8      9     private static final JedisPool pool;10     11     static{12         ResourceBundle bundle = ResourceBundle.getBundle("redis");13         JedisPoolConfig config = new JedisPoolConfig();14         if (bundle == null) {    15             throw new IllegalArgumentException("[redis.properties] is not found!");    16         }17         //设置池配置项值  18         config.setMaxActive(Integer.valueOf(bundle.getString("jedis.pool.maxActive")));    19         config.setMaxIdle(Integer.valueOf(bundle.getString("jedis.pool.maxIdle")));    20         config.setMaxWait(Long.valueOf(bundle.getString("jedis.pool.maxWait")));    21         config.setTestOnBorrow(Boolean.valueOf(bundle.getString("jedis.pool.testOnBorrow")));    22         config.setTestOnReturn(Boolean.valueOf(bundle.getString("jedis.pool.testOnReturn")));23         24         pool = new JedisPool(config, bundle.getString("redis.ip"),Integer.valueOf(bundle.getString("redis.port")) );25     }26     27     /**28      * 29      * @Title: release30      * @Description: 释放连接31      * @param @param jedis32      * @return void33      * @throws34      */35     public static void release(Jedis jedis){36         pool.returnResource(jedis);37     }38     39     public static Jedis getJedis(){40         return pool.getResource();41     }42 43 }
复制代码

  Redis在线列表工具类:

复制代码
  1 import java.util.ArrayList;  2 import java.util.Collections;  3 import java.util.Comparator;  4 import java.util.Date;  5 import java.util.List;  6 import java.util.Set;  7   8 import net.sf.json.JSONObject;  9 import net.sf.json.JsonConfig; 10 import net.sf.json.processors.JsonValueProcessor; 11  12 import cn.sccl.common.util.StringUtil; 13  14 import com.xxx.common.util.JsonDateValueProcessor; 15 import com.xxx.user.model.ClientUser; 16  17 import redis.clients.jedis.Jedis; 18 import redis.clients.jedis.Pipeline; 19 import tools.Constants; 20  21 /** 22  *  23  * Redis缓存中存放两组key: 24  * 1.SID_PREFIX开头,存放登陆用户的SessionId与ClientUser的Json数据 25  * 2.UID_PREFIX开头,存放登录用户的UID与SessionId对于的数据 26  * 27  * 3.VID_PREFIX开头,存放位于指定页面用户的数据(与Ajax一起使用,用于实现指定页面同时浏览人数的限制功能) 28  *  29  * @ClassName: OnlineUtils 30  * @Description: 在线列表操作工具类 31  * @author BuilderQiu 32  * @date 2014-1-9 上午09:25:43 33  * 34  */ 35 public class OnlineUtils { 36      37     //KEY值根据SessionID生成     38     private static final String SID_PREFIX = "online:sid:"; 39     private static final String UID_PREFIX = "online:uid:"; 40     private static final String VID_PREFIX = "online:vid:"; 41     private static final int OVERDATETIME = 30 * 60; 42     private static final int BROADCAST_OVERDATETIME = 70;//Ajax每60秒发起一次,超过BROADCAST_OVERDATETIME时间长度未发起表示已经离开该页面 43  44     public static void login(String sid,ClientUser user){ 45          46         Jedis jedis = RedisPoolUtils.getJedis(); 47  48         jedis.setex(SID_PREFIX+sid, OVERDATETIME, userToString(user)); 49         jedis.setex(UID_PREFIX+user.getId(), OVERDATETIME, sid); 50          51         RedisPoolUtils.release(jedis); 52     } 53      54     public static void broadcast(String uid,String identify){ 55          56         if(uid==null||"".equals(uid)) //异常数据,正常情况下登陆用户才会发起该请求 57             return ; 58          59         Jedis jedis = RedisPoolUtils.getJedis(); 60          61         jedis.setex(VID_PREFIX+identify+":"+uid, BROADCAST_OVERDATETIME, uid); 62          63         RedisPoolUtils.release(jedis); 64     } 65      66      67     private static String userToString(ClientUser user){ 68         JsonConfig  config = new JsonConfig(); 69         JsonValueProcessor processor = new JsonDateValueProcessor("yyyy-MM-dd HH:mm:ss"); 70         config.registerJsonValueProcessor(Date.class, processor); 71         JSONObject obj = JSONObject.fromObject(user, config); 72  73         return obj.toString(); 74     } 75      76     /** 77      *  78      * @Title: logout 79      * @Description: 退出 80      * @param @param sessionId 81      * @return void 82      * @throws 83      */ 84     public static void logout(String sid,String uid){ 85          86         Jedis jedis = RedisPoolUtils.getJedis(); 87          88         jedis.del(SID_PREFIX+sid); 89         jedis.del(UID_PREFIX+uid); 90          91         RedisPoolUtils.release(jedis); 92     } 93      94     /** 95      *  96      * @Title: logout 97      * @Description: 退出 98      * @param @param UserId  使指定用户下线 99      * @return void100      * @throws101      */102     public static void logout(String uid){103         Jedis jedis = RedisPoolUtils.getJedis();104         105         //删除sid106         jedis.del(SID_PREFIX+jedis.get(UID_PREFIX+uid));107         //删除uid108         jedis.del(UID_PREFIX+uid);109         110         RedisPoolUtils.release(jedis);111     }112     113     public static String getClientUserBySessionId(String sid){114         115         Jedis jedis = RedisPoolUtils.getJedis();116         117         String user = jedis.get(SID_PREFIX+sid);118         119         RedisPoolUtils.release(jedis);120         121         return user;122     }123     124     public static String getClientUserByUid(String uid){125         Jedis jedis = RedisPoolUtils.getJedis();126         127         String user = jedis.get(SID_PREFIX+jedis.get(UID_PREFIX+uid));128         129         RedisPoolUtils.release(jedis);130         131         return user;132     }133     134     /**135      * 136      * @Title: online137      * @Description: 所有的key138      * @return List  139      * @throws140      */141     public static List online(){142 143         Jedis jedis = RedisPoolUtils.getJedis();144         145         Set online = jedis.keys(SID_PREFIX+"*");146         147         RedisPoolUtils.release(jedis);148         return new ArrayList(online);149     }150     151     /**152      * 153      * @Title: online154      * @Description: 分页显示在线列表155      * @return List  156      * @throws157      */158     public static List onlineByPage(int page,int pageSize) throws Exception{159         160         Jedis jedis = RedisPoolUtils.getJedis();161         162         Set onlineSet = jedis.keys(SID_PREFIX+"*");163         164         165         List onlines =new ArrayList(onlineSet);166         167         if(onlines.size() == 0){168             return null;169         }170         171         Pipeline pip = jedis.pipelined();172         for(Object key:onlines){173             pip.get(getKey(key));174         }175         List result = pip.syncAndReturnAll();176         RedisPoolUtils.release(jedis);177         178         List<ClientUser> listUser=new ArrayList<ClientUser>();179         for(int i=0;i<result.size();i++){180             listUser.add(Constants.json2ClientUser((String)result.get(i)));181         }182         Collections.sort(listUser,new Comparator<ClientUser>(){183             public int compare(ClientUser o1, ClientUser o2) {184                 return o2.getLastLoginTime().compareTo(o1.getLastLoginTime());185             }186         });187         onlines=listUser;188         int start = (page - 1) * pageSize;189         int toIndex=(start+pageSize)>onlines.size()?onlines.size():start+pageSize;190         List list = onlines.subList(start, toIndex);191     192         return list;193     }194     195     private static String getKey(Object obj){196         197         String temp = String.valueOf(obj);198         String key[] = temp.split(":");199 200         return SID_PREFIX+key[key.length-1];201     }202     203     /**204      * 205      * @Title: onlineCount206      * @Description: 总在线人数207      * @param @return208      * @return int209      * @throws210      */211     public static int onlineCount(){212         213         Jedis jedis = RedisPoolUtils.getJedis();214         215         Set online = jedis.keys(SID_PREFIX+"*");216         217         RedisPoolUtils.release(jedis);218         219         return online.size();220         221     }222     223     /**224      * 获取指定页面在线人数总数225      */226     public static int broadcastCount(String identify) {227         Jedis jedis = RedisPoolUtils.getJedis();228         229         Set online = jedis.keys(VID_PREFIX+identify+":*");230         231         232 233         RedisPoolUtils.release(jedis);234         235         return online.size();236     }237     238     /**239      * 自己是否在线240      */241     public static boolean broadcastIsOnline(String identify,String uid) {242         243         Jedis jedis = RedisPoolUtils.getJedis();244         245         String online = jedis.get(VID_PREFIX+identify+":"+uid);246         247         RedisPoolUtils.release(jedis);248         249         return !StringUtil.isBlank(online);//不为空就代表已经找到数据了,也就是上线了250     }251     252     /**253      * 获取指定页面在线人数总数254      */255     public static int broadcastCount() {256         Jedis jedis = RedisPoolUtils.getJedis();257         258         Set online = jedis.keys(VID_PREFIX+"*");259         260         RedisPoolUtils.release(jedis);261         262         return online.size();263     }264     265     266     /**267      * 268      * @Title: isOnline269      * @Description: 指定账号是否登陆270      * @param @param sessionId271      * @param @return272      * @return boolean 273      * @throws274      */275     public static boolean isOnline(String uid){276         277         Jedis jedis = RedisPoolUtils.getJedis();278         279         boolean isLogin = jedis.exists(UID_PREFIX+uid);280         281         RedisPoolUtils.release(jedis);282         283         return isLogin;284     }285     286     public static boolean isOnline(String uid,String sid){287         288         Jedis jedis = RedisPoolUtils.getJedis();289         290         String loginSid = jedis.get(UID_PREFIX+uid);291         292         RedisPoolUtils.release(jedis);293         294         return sid.equals(loginSid);295     }296 }
复制代码

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

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

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

复制代码
 1 ... 2 if(uid != null){//已登录 3     if(!OnlineUtils.isOnline(uid, session.getId())){ 4         session.invalidate(); 5  6         return ai.invoke(); 7     }else{ 8         OnlineUtils.login(session.getId(), (ClientUser)session.getAttribute("clientUser")); 9         //刷新缓存10     }11 }12 ...
复制代码

注:Redis在线列表工具类中的部分代码是后来需要实现限制同时访问指定页面浏览人数功能而添加的,同样基于Redis实现,前端由Ajax轮询来更新用户停留页面的状态。

附录:

  Redis连接池配置文件:

复制代码
 1 ###redis##config######## 2 #redis服务器ip #  3 #redis.ip=localhost 4 #redis服务器端口号# 5 redis.port=6379 6  7 ###jedis##pool##config### 8 #jedis的最大分配对象# 9 jedis.pool.maxActive=102410 #jedis最大保存idel状态对象数 #11 jedis.pool.maxIdle=20012 #jedis池没有对象返回时,最大等待时间 #13 jedis.pool.maxWait=100014 #jedis调用borrowObject方法时,是否进行有效检查#15 jedis.pool.testOnBorrow=true16 #jedis调用returnObject方法时,是否进行有效检查 #17 jedis.pool.testOnReturn=true
复制代码
0 0