Spring-Session+Redis实现session共享 实现统计在线人数和踢除用户下线。
首先添加pom相关依赖
<!--spring session 依赖 --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> <version>${spring-session.version}</version> </dependency> <!--spring redis依赖--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>${spring.data.redis.version}</version> </dependency>
web.xml中配置springSessionRepositoryFilter
<!-- redis共享session --> <filter> <filter-name>springSessionRepositoryFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSessionRepositoryFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
创建spring-redis.xml并于spring.xml中引入。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <!-- redis config start --> <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="${redis.maxIdle}"/> <property name="maxTotal" value="${redis.maxActive}"/> <property name="maxWaitMillis" value="${redis.maxWait}"/> <property name="testOnBorrow" value="${redis.testOnBorrow}"/> </bean> <bean id="jedisConnFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:use-pool="true" p:hostName="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:database="${redis.dbIndex}" p:poolConfig-ref="poolConfig"/> <!-- stringRedisTemplate--> <bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate"> <property name="connectionFactory" ref="jedisConnFactory"/> </bean> <!-- redis template definition --> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnFactory"/> <!--如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!! --> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="hashKeySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="hashValueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/> </property> <!--开启事务 --> <property name="enableTransactionSupport" value="true"></property> </bean> <bean id="JdkSerializationRedisSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/> <bean id="GenericToStringSerializer" class="org.springframework.data.redis.serializer.GenericToStringSerializer" c:type="java.lang.String"/> <bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager"> <constructor-arg name="redisOperations" ref="redisTemplate"/> <property name="defaultExpiration" value="${redis.expiration}"/> </bean> <bean id="redisCacheConfig" class="com.aloha.app.core.config.RedisCacheConfig"> <constructor-arg ref="jedisConnFactory"/> <constructor-arg ref="redisTemplate"/> <constructor-arg ref="redisCacheManager"/> </bean> <bean id="SessionListener" class="com.aloha.app.root.listener.SessionListener"/> <!-- redis共享程序session --> <util:constant static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/> <bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="maxInactiveIntervalInSeconds" value="1800"/> <property name="redisNamespace" value="ianbase"/> <property name="configureRedisAction" value="NO_OP"/> <property name="httpSessionListeners"> <list> <ref bean="SessionListener" /> </list> </property> </bean><!-- redis config end --> <!--自定义redis工具类,在需要缓存的地方注入此类 --> <bean id="redisUtil" class="com.aloha.app.core.utils.RedisUtil"> <property name="redisTemplate" ref="redisTemplate"/> </bean> </beans>
在spring.xml中引入redis配置文件 redis.properties 并引入spring-redis.xml (spring.xml在此就不赘言了,老生常谈。)
<!-- 引入properties属性文件 --> <context:property-placeholder location="classpath:jdbc/jdbc.properties , classpath:redis/redis.properties"/> <!-- 引入spring redis整合文件 --> <import resource="classpath:spring/spring-redis.xml"/>
redis.properties 配置
# Redis settings
redis.host=127.0.0.1
redis.port=6379
redis.pass=foobared
redis.dbIndex=0
redis.expiration=3000
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true
如果按照如上配置,则需要配置redis的密码为 foobared (可自行定义)
因为笔者使用的是windows版本,可以通过两种方式来配置redis的密码
1.修改redis目录下 redis.windows.conf 中的 requirepass 配置项
配置完成后通过auth 命令登录
配置springSession监听
sessionListener的实现。
package com.aloha.app.root.listener;
import javax.annotation.Resource;
import javax.servlet.ServletContext;
import javax.servlet.http.*;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.aloha.app.authority.constant.AuthorityConstant;
import com.aloha.app.authority.entity.SysUser;
import com.aloha.app.root.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.*;
/**
* com.aloha.app.root.listener
*
* @author zgl
* @name SessionListener
* @description
* @date 2018-04-11 14:10
* <p>
* <p>
* Copyright (c) 2018山东安合信达电子科技有限公司 版权所有
* shandong aloha CO.,LTD. All Rights Reserved.
*/
public class SessionListener implements HttpSessionListener {
/**
* 当前用户
*/
public static final String CURRENT_USER = "CURRENT_USER";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* session创建
*
* @param event
*/
@Override
public void sessionCreated(HttpSessionEvent event) {
HttpSession session = event.getSession();
// System.out.println(session.getAttribute(AuthorityConstant.VERIFICATION)); null 获取不到
//去redis中根据sessionId获取
Set set = redisTemplate.keys("*" + session.getId() + "*");
ArrayList list = new ArrayList(set);
for (int i = 0; i < list.size(); i++) {
String key = list.get(i).toString();
//判断是否hash类型 并且判断是否是有效的用户session
if (redisTemplate.type(key) == DataType.HASH) {
Map<String, Object> map = redisTemplate.opsForHash().entries(key);
System.out.println(redisTemplate.opsForHash().entries(key));
List<String> mapKeys = new ArrayList<String>(map.keySet());
for (int j = 0; j < mapKeys.size(); j++) {
System.out.println(mapKeys.get(j) + ": " + map.get(mapKeys.get(j)));
if (mapKeys.get(j).indexOf(AuthorityConstant.VERIFICATION) != -1) {
System.out.println("验证码产生的session不加用户数");
break;
} else if (mapKeys.get(j).indexOf(AuthorityConstant.SESSION_USER_CODE) != -1 && mapKeys.get(j).indexOf("userId") ==-1 ) {
System.out.println(mapKeys.get(j)+"--------------------------------");
System.out.println("用户登录后产生的session增加用户数");
//存储用户hash
saveUserOnlineHash(session.getId().toString(),(SysUser) map.get(mapKeys.get(j)));
break;
}
}
}
}
// logger.info("创建了一个Session连接:[" + session.getId() + "]");
System.out.println("创建了一个Session连接:[" + session.getId() + "]");
}
/**
* session销毁
*
* @param event
*/
@Override
public void sessionDestroyed(HttpSessionEvent event) {
boolean flag = true;
HttpSession session = event.getSession();
Set<String> keys = redisTemplate.keys("*" + session.getId() + "*");
ArrayList removelist = new ArrayList(keys);
for (int i = 0; i < removelist.size(); i++) {
String key = removelist.get(i).toString();
//判断是否hash类型 并且判断是否是有效的用户session
if (redisTemplate.type(key) == DataType.HASH) {
Map<String, Object> map = redisTemplate.opsForHash().entries(key);
//System.out.println(redisTemplate.opsForHash().entries(key));
List<String> mapKeys = new ArrayList<String>(map.keySet());
for (int j = 0; j < mapKeys.size(); j++) {
//System.out.println(mapKeys.get(j) + ": " + map.get(mapKeys.get(j)));
if (mapKeys.get(j).indexOf(AuthorityConstant.VERIFICATION) != -1) {
System.out.println("验证码产生的session不减用户数");
flag = false;
break;
}
}
}
}
redisTemplate.delete(keys);
Set set = redisTemplate.keys("spring:session:ianbase:expirations*");
ArrayList list = new ArrayList(set);
for (int i = 0; i < list.size(); i++) {
String key = String.valueOf(list.get(i));
//删除set
if (redisTemplate.type(key) == DataType.SET) {
SetOperations<String, String> vo = redisTemplate.opsForSet();
Iterator<String> it = vo.members(key).iterator();
while (it.hasNext()) {
if (it.next().indexOf(session.getId().toString()) != -1) {
System.out.println(key+"................");
redisTemplate.delete(key);
//System.out.println(session.getId().toString());
System.out.println("删除成功了");
}
}
}
}
System.out.println("销毁了一个Session连接:[" + session.getId() + "]");
if (getAllUserNumber() > 0 && flag) {
removeOnlineHashBySessionId(session.getId().toString());
}
}
/**
* 保存用户数
*
* @param n
*/
private void setAllUserNumber(int n) {
Long number = getAllUserNumber() + n;
if (number >= 0) {
// logger.info("用户数:" + number);
System.out.println("用户数:" + number);
stringRedisTemplate.opsForValue().set(AuthorityConstant.ALLUSER_NUMBER, String.valueOf(number));
}
}
/**
* 获取登录用户数目
*
* @return
*/
public Long getAllUserNumber() {
Object v = stringRedisTemplate.opsForValue().get(AuthorityConstant.ALLUSER_NUMBER);
if (v != null) {
return Long.valueOf(v.toString());
}
return 0L;
}
public void saveUserOnlineHash(String sessionId,SysUser sysUser){
Map<String,SysUser> onlineUsers = null;
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
if(redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS)!=null){
onlineUsers = redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS);
if(!onlineUsers.keySet().contains(sessionId)){
setAllUserNumber(+1);
}
onlineUsers.put(sessionId,sysUser);
redisTemplate.opsForHash().putAll(AuthorityConstant.ONLINE_USERS,onlineUsers);
}else{
onlineUsers = new HashMap<>();
if(!onlineUsers.keySet().contains(sessionId)){
setAllUserNumber(+1);
}
onlineUsers.put(sessionId,sysUser);
redisTemplate.opsForHash().putAll(AuthorityConstant.ONLINE_USERS,onlineUsers);
}
}
public void removeOnlineHashBySessionId(String sesssionId){
if(redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS)!=null){
Map<String,SysUser> onlineUsers = redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS);
if(onlineUsers.keySet().contains(sesssionId)){
setAllUserNumber(-1);
}
onlineUsers.remove(sesssionId);
redisTemplate.delete(AuthorityConstant.ONLINE_USERS);
redisTemplate.opsForHash().putAll(AuthorityConstant.ONLINE_USERS,onlineUsers);
}
}
}
/** * session销毁 * * @param event */ @Override public void sessionDestroyed(HttpSessionEvent event) { boolean flag = true; HttpSession session = event.getSession(); Set<String> keys = redisTemplate.keys("*" + session.getId() + "*"); ArrayList removelist = new ArrayList(keys); for (int i = 0; i < removelist.size(); i++) { String key = removelist.get(i).toString(); //判断是否hash类型 并且判断是否是有效的用户session if (redisTemplate.type(key) == DataType.HASH) { Map<String, Object> map = redisTemplate.opsForHash().entries(key); //System.out.println(redisTemplate.opsForHash().entries(key)); List<String> mapKeys = new ArrayList<String>(map.keySet()); for (int j = 0; j < mapKeys.size(); j++) { //System.out.println(mapKeys.get(j) + ": " + map.get(mapKeys.get(j))); if (mapKeys.get(j).indexOf(AuthorityConstant.VERIFICATION) != -1) { System.out.println("验证码产生的session不减用户数"); flag = false; break; } } } } redisTemplate.delete(keys); Set set = redisTemplate.keys("spring:session:ianbase:expirations*"); ArrayList list = new ArrayList(set); for (int i = 0; i < list.size(); i++) { String key = String.valueOf(list.get(i)); //删除set if (redisTemplate.type(key) == DataType.SET) { SetOperations<String, String> vo = redisTemplate.opsForSet(); Iterator<String> it = vo.members(key).iterator(); while (it.hasNext()) { if (it.next().indexOf(session.getId().toString()) != -1) { System.out.println(key+"................"); redisTemplate.delete(key); //System.out.println(session.getId().toString()); System.out.println("删除成功了"); } } } } System.out.println("销毁了一个Session连接:[" + session.getId() + "]"); if (getAllUserNumber() > 0 && flag) { removeOnlineHashBySessionId(session.getId().toString()); setAllUserNumber(-1); } } /** * 保存用户数 * * @param n */ private void setAllUserNumber(int n) { Long number = getAllUserNumber() + n; if (number >= 0) { // logger.info("用户数:" + number); System.out.println("用户数:" + number); stringRedisTemplate.opsForValue().set(AuthorityConstant.ALLUSER_NUMBER, String.valueOf(number)); } } /** * 获取登录用户数目 * * @return */ public Long getAllUserNumber() { Object v = stringRedisTemplate.opsForValue().get(AuthorityConstant.ALLUSER_NUMBER); if (v != null) { return Long.valueOf(v.toString()); } return 0L; } public void saveUserOnlineHash(String sessionId,SysUser sysUser){ Map<String,SysUser> onlineUsers = null; redisTemplate.setHashKeySerializer(new StringRedisSerializer()); if(redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS)!=null){ onlineUsers = redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS); onlineUsers.put(sessionId,sysUser); redisTemplate.opsForHash().putAll(AuthorityConstant.ONLINE_USERS,onlineUsers); }else{ onlineUsers = new HashMap<>(); onlineUsers.put(sessionId,sysUser); redisTemplate.opsForHash().putAll(AuthorityConstant.ONLINE_USERS,onlineUsers); } } public void removeOnlineHashBySessionId(String sesssionId){ if(redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS)!=null){ Map<String,SysUser> onlineUsers = redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS); onlineUsers.remove(sesssionId); redisTemplate.delete(AuthorityConstant.ONLINE_USERS); redisTemplate.opsForHash().putAll(AuthorityConstant.ONLINE_USERS,onlineUsers); } } }
具体的逻辑就不多说了 相信大家都能看懂,比较基础的代码。
重点说一下这里的坑!
程序跑起来以后发现 session的销毁方法没有监听到!!!!
解决方法:
查阅许多博客后发现 redis的 notify-keyspace-events 配置没有打开 貌似是 空间事件监听之类的 具体的没有深究。
然后配置 notify-keyspace-events 为KEA
详细的请参考
https://blog.csdn.net/gqtcgq/article/details/50808729
就此完成了登录用户信息的保存,和在线人数的统计。
查看在线人数和踢除用户下线的接口:
@GetMapping("onlineUser") public Result onlineUserCount(HttpSession session) { List<JSONObject> users = new ArrayList<>(); String count = stringRedisTemplate.opsForValue().get(AuthorityConstant.ALLUSER_NUMBER); Map<String, Object> map = redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS); List<String> mapKeys = new ArrayList<String>(map.keySet()); for (int j = 0; j < mapKeys.size(); j++) { // System.out.println(mapKeys.get(j) + ": " + session.getId().toString()); JSONObject jsonObject = JSONUtil.parseObj(map.get(mapKeys.get(j))); if (mapKeys.get(j).equals(session.getId().toString())) { jsonObject.put("iscurr", true); } jsonObject.put("sessionId", mapKeys.get(j)); users.add(jsonObject); } return Result.ok().put("data", users).put("count", StringUtils.isEmpty(count) ? "0" : count); } @PostMapping("downline") public Result downline(@RequestBody String sid) { sid= sid.replaceAll("=",""); Map<String, SysUser> map = redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS); Set<String> keys = redisTemplate.keys("*" + sid + "*"); redisTemplate.delete(keys); Set set = redisTemplate.keys("spring:session:ianbase:expirations*"); ArrayList list = new ArrayList(set); for (int i = 0; i < list.size(); i++) { String key = String.valueOf(list.get(i)); //删除set if (redisTemplate.type(key) == DataType.SET) { SetOperations<String, String> vo = redisTemplate.opsForSet(); Iterator<String> it = vo.members(key).iterator(); while (it.hasNext()) { if (it.next().indexOf(sid.toString()) != -1) { redisTemplate.delete(key); removeOnlineHashBySessionId(sid); //减用户数 long count = Long.parseLong(stringRedisTemplate.opsForValue().get(AuthorityConstant.ALLUSER_NUMBER)) - 1; stringRedisTemplate.opsForValue().set(AuthorityConstant.ALLUSER_NUMBER, String.valueOf(count)); System.out.println("强制下线成功"); } } } } return Result.ok("已强制该用户下线。"); } public void removeOnlineHashBySessionId(String sesssionId) { if (redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS) != null) { Map<String, SysUser> onlineUsers = redisTemplate.opsForHash().entries(AuthorityConstant.ONLINE_USERS); onlineUsers.remove(sesssionId); redisTemplate.delete(AuthorityConstant.ONLINE_USERS); redisTemplate.opsForHash().putAll(AuthorityConstant.ONLINE_USERS, onlineUsers); } }
sid 为前台传过来的对应用户的sessionid
然后这里扩展一点。负载均衡宕机情况下在线用户人数没有对应改变该如何处理呢?
这里简单说一下,笔者通过两种方式去实现了一下。
1.监听spring的生命周期实现redis数据
2.监听tomcat的开启关闭时间。
二者选其一即可 但缺点是 只有调用tomcat 的shutdown脚本时才会触发销毁。
因为此时笔者所做项目对这块没有什么要求 因此选用了第一种实现方式。
见代码如下:
@Component public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedisTemplate redisTemplate; @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { // 防止子容器启动完成后再次调用 if(contextRefreshedEvent.getApplicationContext().getParent() == null){ //判断当前会否有tomcat在运行 如果有则说明tomcat数 的key已经创建 否则重新创建 if(stringRedisTemplate.opsForValue().get(AuthorityConstant.TOMCAT_NUM)!=null){ long count =Long.parseLong(stringRedisTemplate.opsForValue().get(AuthorityConstant.TOMCAT_NUM)); stringRedisTemplate.opsForValue().set(AuthorityConstant.TOMCAT_NUM,String.valueOf(count+1)); }else{ //初始化 stringRedisTemplate.opsForValue().set(AuthorityConstant.TOMCAT_NUM,"1"); } System.out.println("spring容器初始化"); } } @Component public class StopAddDataListener implements ApplicationListener<ContextClosedEvent> { public void onApplicationEvent(ContextClosedEvent contextClosedEvent) { if(contextClosedEvent.getApplicationContext().getParent() == null) { if(stringRedisTemplate.opsForValue().get(AuthorityConstant.TOMCAT_NUM)!=null){ long count =Long.parseLong(stringRedisTemplate.opsForValue().get(AuthorityConstant.TOMCAT_NUM)); if(count-1==0){ //清理redis flushDB(); }else{ stringRedisTemplate.opsForValue().set(AuthorityConstant.TOMCAT_NUM,String.valueOf(count-1)); } } System.out.println("spring容器关闭"); } } } public void flushDB() { redisTemplate.getConnectionFactory().getConnection().flushDb(); } }
好吧,就写到这里吧,希望对有需要的人提供一定的帮助。
SessionListener 的实现