`
阅读更多

前言:

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

  在单机环境下,在线列表的实现方案可以采用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: SessionListener
14  * @Description: 记录所有登陆的Session信息,为在线列表做基础
15  * @author BuilderQiu
16  * @date 2013-9-18 09:35:13
17  *
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: isLoginOnThisSession
80      * @Description: 检测是否已经登陆
81      * @param @param uid 用户UserId
82      * @param @param sid 发起请求的用户的SessionId
83      * @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: release
30      * @Description: 释放连接
31      * @param @param jedis
32      * @return void
33      * @throws
34      */
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 void
100      * @throws
101      */
102     public static void logout(String uid){
103         Jedis jedis = RedisPoolUtils.getJedis();
104         
105         //删除sid
106         jedis.del(SID_PREFIX+jedis.get(UID_PREFIX+uid));
107         //删除uid
108         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: online
137      * @Description: 所有的key
138      * @return List  
139      * @throws
140      */
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: online
154      * @Description: 分页显示在线列表
155      * @return List  
156      * @throws
157      */
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: onlineCount
206      * @Description: 总在线人数
207      * @param @return
208      * @return int
209      * @throws
210      */
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: isOnline
269      * @Description: 指定账号是否登陆
270      * @param @param sessionId
271      * @param @return
272      * @return boolean 
273      * @throws
274      */
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=1024
10 #jedis最大保存idel状态对象数 #
11 jedis.pool.maxIdle=200
12 #jedis池没有对象返回时,最大等待时间 #
13 jedis.pool.maxWait=1000
14 #jedis调用borrowObject方法时,是否进行有效检查#
15 jedis.pool.testOnBorrow=true
16 #jedis调用returnObject方法时,是否进行有效检查 #
17 jedis.pool.testOnReturn=true
分享到:
评论

相关推荐

    Windows 上安装 Redis安装,redis7.2安装到windows上面

    Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows ...

    Redis 7.0.4 x64位 windows 系统 安装包 Redis7.0.4.zip

    Redis7.0.4.zip,解压缩到D盘根目录后,安装后启动为Windows服务 注意是windows 64位系统才可使用,不支持windows 32位系统使用 已经在Win10,Win11,Windows server 2012系统测试运行可用 使用步骤注意事项: ...

    若依前后端分离版去redis版/无redis版本

    基于前后端分离的应用,无论是否使用Redis,都需要考虑如何进行数据的存储和缓存。下面我将分别介绍基于Redis和无Redis的两种版本的特点。 基于Redis的版本 特点 缓存处理:Redis作为内存数据库可以用来缓存频繁访问...

    redis-windows-7.0.11

    Redis是一种开源的内存数据结构存储系统,它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。Redis可以用作数据库、缓存和消息中间件。Redis在性能、可扩展性和灵活性方面表现出色,因此被广泛应用于Web...

    redis-5.0.4.tar.gz下载及redis安装过程

    redis安装 1: 下载redis-5.0.4.tar.gz 2: 解压源码并进入目录 tar zxvf redis-5.0.4.tar.gz cd redis-5.0.4 3: 不用configure 4: 直接make (如果是32位机器 make 32bit) 查看linux机器是32位还是64位的方法:...

    redis-5.0.5.tar.gz

    redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-...

    windows环境下配置redis6集群

    注意:由于本人配置redis.conf中的路径使用的是绝对路径,当前redis安装目录的父目录为D:\InstallDir,可以在每个配置文件中修改为自己指定的路径位置 redis638-.conf配置文件内容 #redis服务器端口号 port 6380 #...

    Redis集群下过期key监听的实现代码

    在使用redis集群时,发现过期key始终监听不到。网上也没有现成的解决方案。于是想,既然不能监听集群,那我可以建立多个redis连接,分别对每个redis的key过期进行监听。以上做法可能不尽人意,目前也没找到好的解决...

    Redis-5.0.0集群配置

    本文档基于以前写的《Redis-3.0.5集群配置》和《Redis-4.0.11集群配置》。  redis-3.0.0开始支持集群,redis-4.0.0开始支持module,redis-5.0.0开始支持类似于kafka那样的消息队列。  本文参考官方文档而成:...

    redis desktop manager(redis桌面管理器)下载(0.8.3)

    redis-desktop-manager-0.8.3.3850.rar windows平台安装文件 Redis Desktop Manager(redis桌面管理器)是一款非常实用的跨平台Redis桌面管理软件。也被称作Redis可视化工具,是一款开源软件,支持通过SSH Tunnel连接...

    Redis面试题50道(含答案)_.pdf

    1、什么是 Redis? 2、Redis 相比 memcached 有哪些优势? 3、Redis 支持哪几种数据类型? 4、Redis 主要消耗什么物理资源? 5、Redis 的全称是什么? 6、Redis 有哪几种数据淘汰策略? 7、Redis 官方为什么不提供 ...

    redis获取命令字典的方法及全部的group命令.txt

    本地安装redis,通过安装目录下的redis客户端程序可以连接任意Redis服务(只要服务端是启动着的),命令 redis-cli -h ip -p port。如果服务端要求认证资格,可能还要输入用户名密码等参数。 Redis命令十分丰富,...

    redis安装离线版免编译版及离线安装需要的所有包文件

    本资源可结合我的文章《详解CentOS7下编译安装Redis5.x设置开机启动及修改端口(含redis需编译安装包和redis免编译安装包)》配合使用,同时也可单独去使用。包含redis3.0.7免编译版安装包,redis5.0.7离线安装包,...

    SpringBoot+VUE+Redis+nginx 网上拍卖平台源码.zip

    SpringBoot+VUE+Redis+nginx 网上拍卖平台源码 SpringBoot+VUE+Redis+nginx 网上拍卖平台源码 SpringBoot+VUE+Redis+nginx 网上拍卖平台源码 SpringBoot+VUE+Redis+nginx 网上拍卖平台源码 SpringBoot+...

    redis_4.0.10-1_arm64.deb 银河麒麟v4+飞腾

    1、redis_4.0.10-1_arm64.deb 银河麒麟v4+飞腾 安装包 2、自带服务启动 3、目录树 /opt/redis-4.0.10/ ├── bin │ ├── redis-benchmark │ ├── redis-check-aof │ ├── redis-check-rdb │ ├── ...

    redis视频教程百度网盘下载地址及密码

    2016122201_Redis_初探_1.mp4 2016122201_Redis_初探_2.mp4 2016122201_Redis_初探_3.mp4 2016122202_Redis_安装_字符串键_1.mp4 2016122202_Redis_安装_字符串键_2.mp4 2016122202_Redis_安装_字符串键_3.mp4 ...

    redis-x64-4.0.2.3,windows版本

    右键解压Redis压缩包,把所有文件解压到指定的redis文件夹中 1.配置redis.windows.conf文件,除了port配置为对应的端口外,其他根据需要可更改的配置项说明如下: (1)bind 127.0.0.1 (line:79) # 设置为其他机器...

    redis桌面链接工具redis-desktop

    redis桌面链接工具redis-desktop Redis Desktop Manager(简称RDM)是一款跨平台的Redis数据库管理工具,它提供了一个直观且用户友好的图形界面,允许开发者和运维人员轻松地连接到Redis服务器,并对其进行管理和...

    1.2.6版StackExchange.Redis

    1.2.6版StackExchange.Redis。 StackExchange.Redis是.net下的Redis客户端之一,目前是免费的。而用户量比较多的ServiceStack.Redis现在是收费的。把下载的三个文件(StackExchange.Redis.dll、StackExchange.Redis....

    Redis入门指南(第2版)电子书

    《Redis入门指南(第2版)》是一本Redis的入门指导书籍,以通俗易懂的方式介绍了Redis基础与实践方面的知识,包括历史与特性、在开发和生产环境中部署运行Redis、数据类型与命令、使用Redis实现队列、事务、复制、...

Global site tag (gtag.js) - Google Analytics