前言:
由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能:
在单机环境下,在线列表的实现方案可以采用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
相关推荐
Redis 是一个开源的内存数据结构存储系统,常被用作数据库、缓存和消息代理。在Windows环境下,Redis 的安装和使用与在Linux系统中有所不同。这里我们将详细讨论Windows版Redis 5.0.14的相关知识点。 1. **Redis ...
最近需要在C#中使用Redis,在Redis的官网找到了ServiceStack.Redis,最后在测试的时候发现这是个坑,4.0已上已经收费,后面只好找到3系列的最终版本,最后测试发现还是有BUG或者是我不会用。没有办法,最好找到了...
在Linux系统中安装Redis是一个常见的任务,特别是在搭建服务器或开发基于Redis的数据缓存应用时。Redis是一个开源的、高性能的键值对存储系统,适用于数据缓存、消息队列等多种场景。本文将详细介绍如何在Linux上...
Redis,全称Remote Dictionary Server,是一款开源的、高性能的键值存储系统,广泛应用于缓存、消息队列、数据持久化等多种场景。它以其高效、轻量级的特性,在IT行业中备受青睐,尤其是在互联网领域。在Windows环境...
redis部署6.2.6最新稳定版文档和程序redis部署6.2.6最新稳定版文档和程序redis部署6.2.6最新稳定版文档和程序redis部署6.2.6最新稳定版文档和程序redis部署6.2.6最新稳定版文档和程序redis部署6.2.6最新稳定版文档和...
Redis是一款高性能的键值对数据库,常用于缓存、消息队列等场景。在Windows操作系统上运行Redis,通常需要借助一些额外的工具。标题提到的"redis在win上的运行脚本redis.bat"就是一个帮助用户在Windows环境下启动...
Redis是世界上最受欢迎的开源内存数据结构存储系统,它可以用作数据库、缓存和消息代理。在Windows 64位环境下,Redis的安装和卸载过程是很多开发者和系统管理员需要了解的重要技能。以下是对这些知识点的详细说明:...
Redis Desktop Manager是一款强大的开源图形化界面工具,专为管理和操作Redis键值存储系统而设计。它为用户提供了直观且高效的界面,使得在处理Redis数据库时能够更加便捷。这个压缩包包含的就是这款工具的安装文件...
Redis是一款高性能的键值对内存数据库,被广泛应用于缓存、数据存储等领域。在这个Windows 64位版本的Redis 7.0.5中,我们能够看到一系列关键组件和配置文件,这使得它能够在Windows环境下运行。以下是关于Redis ...
基于前后端分离的应用,无论是否使用Redis,都需要考虑如何进行数据的存储和缓存。下面我将分别介绍基于Redis和无Redis的两种版本的特点。 基于Redis的版本 特点 缓存处理:Redis作为内存数据库可以用来缓存频繁访问...
Redis7.0.4.zip,解压缩到D盘根目录后,安装后启动为Windows服务 注意是windows 64位系统才可使用,不支持windows 32位系统使用 已经在Win10,Win11,Windows server 2012系统测试运行可用 使用步骤注意事项: ...
在Windows上安装Redis的过程涉及到多个步骤,包括启用必要的Windows功能、安装WSL2(Windows Subsystem for Linux 2)、设置默认WSL版本以及在Linux环境中安装Redis。以下是对这些步骤的详细说明: 1. **启用...
Redis 是一个高性能的键值数据库,它以键值对的形式存储数据,广泛应用于缓存、消息中间件、实时分析等领域。在 Windows 环境下,通常需要通过安装过程来设置 Redis 服务,但这里提供的资源是“redis 免安装”,意味...
Redis,全称Remote Dictionary Server,是一款高性能的键值存储数据库,常用于缓存、消息队列等场景。本文将深入探讨Redis的可视化工具及其免安装绿色版的使用,帮助你更好地管理和操作Redis服务器。 首先,了解...
Redis 使用教程详解 Redis 是一个高性能的 NoSQL 键值存储数据库,广泛应用于缓存、任务列表、网站访问统计数据、过期处理、应用排行榜、分布式集群架构中的 session 分离等领域。下面是 Redis 的详细使用教程。 ...
(1)支持Lua脚本:Redis 3.0支持Lua脚本,可以在Redis中执行脚本,大大提高了Redis的灵活性和可扩展性; (2)可插拔模块化:Redis 3.0提供了可插拔的模块化功能,可以根据用户的需求,自定义模块,实现不同的功能...
"Redis++使用说明,windows下编译Redis-Plus-Plus" 在这篇文章中,我们将详细介绍如何在Windows平台下编译Redis++,包括编译hiredis.lib和Win32_Interop.lib静态库文件的过程,然后安装Cmake并编译Redis++,最后...
Redis是一款高性能的键值存储系统,常用于数据库、缓存和消息代理等场景。它支持丰富的数据类型,如字符串、哈希、列表、集合和有序集合。在Windows平台上使用Redis,通常需要通过编译源码或者寻找预编译的二进制...
ASP.NET Core 使用 Redis 基于 StackExchange.Redis ASP.NET Core 是一个开源的、跨平台的框架,使用 C# 语言开发。Redis 是一个基于内存的数据存储系统,可以用来存储和处理大量数据。StackExchange.Redis 是一个...
Redis是一款高性能的键值对数据库,常用于缓存、消息队列等场景。这个"Redis-6.2.7 Windows 版"是针对Windows操作系统编译优化的版本,旨在为Windows用户提供便捷的服务。以下是关于Redis 6.2.7及其在Windows上的...