迁移:基于Redis的在线用户列表解决方案

前言:

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

  在单机环境下,在线列表的实现方案可以采用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

转载于:https://www.cnblogs.com/warden/p/onlinelist_base_on_redis.html

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

  1. Redis实现在线用户列表(按登录时间排序、可查询、踢人)

    前言 在日常开发中,我们有时候需要对在线用户进行管理,由于登录信息是可以过期的,那么使用RDBMS存储有点不合时宜,所以NoSQL存储更为合理.网上找了一些资料,但貌似似乎都不太完备,以下为个人理解的 ...

  2. ASP.NET在线用户列表精确版——解决用户意外退出在线列表无法及时更新问题

    最近所做的一个项目需要用到的在线用户列表,上网搜索了一下发现现有的解决方案对用户意外退出的处理均不是太理想.一般来说,用户离开系统的方式有三种:主动注销.会话超时.直接关闭浏览器,对于前两种,我们很容 ...

  3. 用redis+jwt保存在线用户和获得在线用户列表、踢出用户示例

    文章目录 redis工具类 用户实体类 token配置 service层保存和查询在线用户 工具类 获得用户浏览器等其他信息 controller层 redis工具类 import org.sprin ...

  4. apache在线升级yum_基于Redis实现在线游戏积分排行榜 - phyger

    介绍 本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能. 背景知识 Redis Redis是一个开源的使用ANSI C语言编写.遵守BSD协议.支持网络.可基于内存亦可持久化 ...

  5. 基于Redis实现在线游戏积分排行榜

    介绍 本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能. 背景知识 Redis Redis是一个开源的使用ANSI C语言编写.遵守BSD协议.支持网络.可基于内存亦可持久化 ...

  6. 基于Redis实现在线游戏积分排行榜!体验一下云服务器!

    基于Redis实现在线游戏积分排行榜 体验地址:https://developer.aliyun.com/adc/scenario/44d54481170f4914996d3ae53b818f32?s ...

  7. JAVA实现QQ:实现文字聊天、QQ用户登录、拉取在线用户列表、无异常退出、私聊、发文件、下载文件、离线留言、服务端推送新闻等功能(后端无界面,Utilty源码在后面、)

    这个仿QQ项目是参考韩顺平老师的多线程课程做的,因为个人觉得非常有意义特别是让我对多线程通信又了一个新的理解因此我准备写一篇总结(如果觉得视频太长可以参考下): 具体视频地址:大家给韩老师一键三连[韩 ...

  8. php redis 签到,基于Redis位图实现用户签到功能

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

  9. [项目回顾]基于Annotation与SpringAOP的缓存简单解决方案

    [项目回顾]基于Annotation与SpringAOP的缓存简单解决方案 参考文章: (1)[项目回顾]基于Annotation与SpringAOP的缓存简单解决方案 (2)https://www. ...

  10. [转载]Asp.Net在线用户列表的開發匯總

    这是转载的别人的一篇,解决了困扰我已久的问题,虽然文章里少了两张图,但是不影响阅读. 1.在线用户列表的实现 在ASP时代,要实现一个网站的在线用户列表显示功能的惯用做法是修改global.asa文件 ...

最新文章

  1. 深蓝学院的深度学习理论与实践课程:第五章
  2. 数据智能与计算机图形学领域推荐论文列表
  3. 文件的权限与隐藏属性
  4. k8s 关键字以及管理流程。
  5. 腾讯新公开这张「图」,我看了眼,上面写满「智驾」二字
  6. CentOS 6.7 RPM安装MySQL
  7. nginx配置图片防盗链
  8. 【五校联考5day1】序列
  9. 实现Android和PC之间的蓝牙通信
  10. java linkedlist源码_Java集合之LinkedList源码分析
  11. 【android】java.net.ConnectException: localhost/127.0.0.1:8080 - Connection refused
  12. 分布式存储绝不简单 —— UCan下午茶-武汉站纪实
  13. 使用自定义功能构建Mamdani系统
  14. 佟年计算机大赛,佟年成电竞高手,老韩带老婆打比赛,solo内网被佟年轻松攻破...
  15. 英語專家談英語學習認識方法
  16. 字符串相似度比较工具
  17. 【论文】GC-MC论文相关
  18. BFS(二)二叉树层序遍历(I、II)、二叉树锯齿形层序遍历、N叉树层序遍历
  19. [论文阅读]PAN++: Towards Efficient and Accurate End-to-End Spotting of Arbitrarily-Shaped Text
  20. 文本分类(text classification)

热门文章

  1. MPLS ××× 的基本配置(二)
  2. cyhper study
  3. 微信宣布:被禁 8 年的限制解除了!
  4. mybatis-plus 官方发布神器,一个依赖轻松搞定数据权限,再也不用自己实现了!...
  5. 「大数据知识体系总结(2021版)」开放下载了!
  6. 阿里P8架构师:淘宝技术架构从1.0到4.0的架构变迁!12页PPT详解
  7. 我经常逛的技术网站,个个经典
  8. IBM大中华区总架构师:话说程序员的职业生涯
  9. 你整明白了吗?Linux Shell 中各种括号的作用 ()、(())、[]、[[]]、{}
  10. 这本值得你认真一读的畅销书特价了