Redis实现同一账号登录个数限制

 

场景:

        最近接到一个需求,为了方便用户使用,系统的同一个用户账号可以在5个浏览器是登录,第六个登录的把第一个挤掉,而且用户登录后长时间不操作是需要自动过期的,也就是说需要有session过期时间;

一、分析

       从需求来看,用户被强制下线有两种可能,一个是session过期,二个是被后面登录的挤掉;第6个登录的把第1个登录的挤掉,那么我们就需要统计登录个数以及找到最早登录的信息;这么一想就联想到了Redis,可以使用Redis的String和ZSet数据结构来实现;

String存储的作用:session过期,需要重新登录;

ZSet存储的作用:后面登录的挤掉最先登录的;

      ZSet具有排序的功能;用户登录后就是一个会话,就会产生一个session,不同的session不同的sessionid,所以:

一、使用sessionID作为key,userName作为value并且设置过期时间存储到redis的String类型的数据结构里;

二、然后把userName作为key,sessionID作为value,存储到ZSet类型的数据结构里,使用系统时间作为分数,设置过期时间;这样,ZSet就会根据时间进行排序,每次登录都先根据userName统计一次ZSet中存储的的个数,如果大于等于5,就删除最早存储的一个。(set里面,更新一个value 剩下的四个value过期时间也都更新,所以,只要同一个账号在过期时间之前登录,set的过期时间就刷新了,可以说,只要string里面存储的sessionId不过期,对应的set就不过期)

      登录时通过redis的zcart方法判断ZSet中username对应的登录数量,如果大于等于5就清除最早登录的一组数据,然后把当前登录的信息存储到redis中。否则,直接存储到redis当中即可。

1、ZSet数据存储结构

Redis实现同一账号登录个数限制_第1张图片

2、ZString数据存储结构

Redis实现同一账号登录个数限制_第2张图片

二、实现

第一步:实现操作String和ZSet的方法

    连接redis这里就不写了,可以使用JedisCluster,也可以使用spring-data-redis针对jedis提供的一个高度封装的“RedisTemplate”类(简单方便),这里使用的是JedisCluster,为了方便操作,这里写了两个操作工具类:

1. String数据结构操作工具类

package com.zhh.redis.util;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import redis.clients.jedis.JedisCluster;

/**
 * Redis 操作工具类 :String数据结构
 * @author  zhaoheng
 * 2018-06-27 
 */
@Component
public class RedisJpaTemplateString {
    private static final Logger log = Logger.getLogger(RedisJpaTemplateString.class.getName());

    @Autowired
    private JedisCluster   jedisCluster;
    
    /**
     * 默认过期时间 (单位:秒)
     */
    private int expireTime = 24*60*60;

    /**
     * 用于隔开缓存前缀与缓存键值 
     */
    private static final String KEY_SPLIT = ":"; 

    /**
     * 设置缓存 
     * @param prefix 缓存前缀(用于区分缓存,防止缓存键值重复)
     * @param key    缓存key
     * @param value  缓存value
     */
    public void setWithPrefix(String prefix, String key, String value) {
        jedisCluster.set(prefix + KEY_SPLIT + key, value);
        log.info("RedisJpaTemplateString:set cache key="+prefix + KEY_SPLIT + key+",value="+value);
    }

    /**
     * 设置缓存,并且自己指定过期时间
     * @param prefix
     * @param key
     * @param value
     * @param expireTime 过期时间(秒)
     */
    public void setWithExpireTime(String prefix, String key, String value, int expireTime) {
        jedisCluster.setex(prefix + KEY_SPLIT + key, expireTime, value);
        log.info("RedisJpaTemplateString:setWithExpireTime cache key="+prefix + KEY_SPLIT + key+",value="+value+",expireTime="+expireTime);
    }

    /**
     * 设置缓存,并且由配置文件指定过期时间
     * @param prefix
     * @param key
     * @param value
     */
    public void setWithExpireTime(String prefix, String key, String value) {
        jedisCluster.setex(prefix + KEY_SPLIT + key, expireTime, value);
        log.info("RedisJpaTemplateString:setWithExpireTime cache key="+prefix + KEY_SPLIT + key+",value="+value+",expireTime="+expireTime);
    }
 /**
     * 设置 key 的过期时间(如果已经存在过期时间则刷新过期时间)
     * @param prefix 缓存前缀
     * @param key 缓存key
     * @param expireTime 过期时间
     * @return 设置成功返回 1
     */
	public Long setOrRefurbishExpireTime(String prefix, String key, int expireTime) {
		  long number = jedisCluster.expire(prefix + KEY_SPLIT + key, expireTime);
	      log.info("RedisJpaTemplateString:setWithExpireTime cache key="+prefix + KEY_SPLIT + key+" expireTime="+expireTime+" 执行返回结果:"+number);
		return number;
	}


    /**
     * 获取指定key的缓存
     * @param prefix
     * @param key
     */
    public String getWithPrefix(String prefix, String key) {
        String value = jedisCluster.get(prefix + KEY_SPLIT + key);
        log.info("RedisJpaTemplateString:get cache key="+prefix + KEY_SPLIT + key+",value="+value);
        return value;
    }

    /**
     * 删除指定key的缓存
     * @param prefix
     * @param key
     */
    public void deleteWithPrefix(String prefix, String key) {
        jedisCluster.del(prefix + KEY_SPLIT + key);
        log.info("RedisJpaTemplateString:delete cache key="+prefix + KEY_SPLIT + key);
    }
      /**
     * 检查给定 key 是否存在 若 key 存在返回 true ,否则返回false 
     * @param prefix 缓存前缀
     * @param key 缓存key
     * @return 
     */
	public Boolean exists(String prefix, String key) {
		Boolean flag = jedisCluster.exists(prefix + KEY_SPLIT + key);
		log.info("RedisJpaTemplateString:exists cache key="+prefix + KEY_SPLIT + key+" 执行返回结果:"+flag);
		return flag;
	}


    public JedisCluster getJedisCluster() {
		return jedisCluster;
	}

	public void setJedisCluster(JedisCluster jedisCluster) {
		this.jedisCluster = jedisCluster;
	}
	public int getExpireTime() {
		return expireTime;
	}
	public void setExpireTime(int expireTime) {
		this.expireTime = expireTime;
	}
}

2. ZSet数据结构操作工具类

package com.zhh.redis.util;

import java.util.Set;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import redis.clients.jedis.JedisCluster;

/**
 * Redis 操作工具类 :ZSet数据结构
 * @author zhaoheng
 * 2018-06-26
 */
@Component
public class RedisJpaTemplateZSet {
	private static final Logger log = Logger.getLogger(RedisJpaTemplateZSet.class.getName());
	
	@Autowired
    private JedisCluster   jedisCluster;
	
	private static final String KEY_SPLIT = ":"; //用于隔开缓存前缀与缓存键值 

	/**
	 * 将带有给定分值的成员添加到有序列表里面
	 * @param prefix 缓存前缀(用于区分缓存,防止缓存键值重复)
	 * @param key 缓存key
	 * @param value 缓存value
	 * @param score 用于排名的数值(分数)
	 * @return 返回添加成功的数量
	 */
	public Long add(String prefix,String key, String value, double score) {
		long number = jedisCluster.zadd(prefix + KEY_SPLIT + key, score, value);
        log.info("ZSetOperationsImpl:add cache key="+prefix + KEY_SPLIT + key+",value="+value+" score="+score+" 执行返回结果:"+number);
        return number;
	}

    /**
	 * 返回有序集合包含的成员数量
	 * @param prefix 缓存前缀
	 * @param key 缓存key
	 * @return
	 */
	public Long size(String prefix,String key) {
        long number = jedisCluster.zcard(prefix + KEY_SPLIT + key);
        log.info("ZSetOperationsImpl:size cache key="+prefix + KEY_SPLIT + key+" 执行返回结果:"+number);
		return number;
	}

	 /**
	  * 返回有序集合中排名介于start和end之间的成员,包括start和end在内
	  * @param prefix 缓存前缀
	  * @param key 缓存key
	  * @param start 排名区间开始值
	  * @param end 排名区间结束值
	  * @return
	  */
	public Set range(String prefix, String key, long start, long end) {
		Set set = jedisCluster.zrange(prefix + KEY_SPLIT + key, start, end);
        log.info("ZSetOperationsImpl:range cache key="+prefix + KEY_SPLIT + key+" start="+start+" end="+end);
		return set;
	}
	
	 /**
	  * 移除有序集合中排名介于start和end之间的所有成员,包括start和end在内,并返回被移除成员的数量
	  * @param prefix 缓存前缀
	  * @param key 缓存key
	  * @param start
	  * @param end
	  * @return 返回被移除的个数
	  */
	public Long removeRange(String prefix, String key, long start, long end) {
		long number = jedisCluster.zremrangeByRank(prefix + KEY_SPLIT + key, start, end);
        log.info("ZSetOperationsImpl:removeRange cache key="+prefix + KEY_SPLIT + key+" start="+start+" end="+end+" 执行返回结果:"+number);
		return number;
	}

    /**
     * 从有序集合里面移除给定的成员,并返回被移除成员的数量
     * @param prefix 缓存前缀
     * @param key 缓存key
     * @param value 缓存value
     * @return
     */
	public Long remove(String prefix, String key, String... value) {
		long number = jedisCluster.zrem(prefix + KEY_SPLIT + key, value);
        log.info("ZSetOperationsImpl:remove cache key="+prefix + KEY_SPLIT + key+" value="+value+" 执行返回结果:"+number);
		return number;
	}

    /**
     * 设置 key 的过期时间(如果已经存在过期时间则刷新过期时间)
     * @param prefix 缓存前缀
     * @param key 缓存key
     * @param expireTime 过期时间
     * @return 设置成功返回 1
     */
	public Long setOrRefurbishExpireTime(String prefix, String key, int expireTime) {
		  long number = jedisCluster.expire(prefix + KEY_SPLIT + key, expireTime);
	      log.info("ZSetOperationsImpl:setOrRefurbishExpireTime cache key="+prefix + KEY_SPLIT + key+" expireTime="+expireTime+" 执行返回结果:"+number);
		return number;
	}

    /**
     * 删除指定key的缓存
     * @param prefix 缓存前缀
     * @param key 缓存key
     * @return 删除成功返回 1
     */
	public Long delete(String prefix, String key) {
		 long number = jedisCluster.del(prefix + KEY_SPLIT + key);
		log.info("ZSetOperationsImpl:delete cache key="+prefix + KEY_SPLIT + key+" 执行返回结果:"+number);
		return number;
	}

    /**
     * 检查给定 key 是否存在 若 key 存在返回 true ,否则返回false 
     * @param prefix 缓存前缀
     * @param key 缓存key
     * @return 
     */
	public Boolean exists(String prefix, String key) {
		Boolean flag = jedisCluster.exists(prefix + KEY_SPLIT + key);
		log.info("ZSetOperationsImpl:exists cache key="+prefix + KEY_SPLIT + key+" 执行返回结果:"+flag);
		return flag;
	}

}

定义常量

public class RedisCodes {
	/**
	 * prefix 缓存前缀(用于区分缓存,防止缓存键值重复) 在用户登录信息存入redis时使用
	 */
	private final String MERCHANT_PREFIX_LOGIN = "LOGIN_";
	
	/**
	 * session过期时间(单位:秒)
	 */
	private final int SESSION_EXPIRE_TIME = 3600;
}

 

第二步、在过滤器里面写相应的逻辑代码

package com.zhh.crm.filter;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.support.WebApplicationContextUtils;

import com.zhh.redis.util.RedisJpaTemplateString;
import com.zhh.redis.util.RedisJpaTemplateZSet;
import com.zhh.redis.util.code.RedisCodes;
import com.zhh.web.common.vo.PiccUser;

/**
 * CRM SESSION超时过滤器,当SESSION超时之后,将由此过滤器返回一个异常编码:600.
 * @author zhaoheng
 */
public class CopyOfCrmSessionFilter implements Filter {

	private ApplicationContext wac;
	private static Logger logger = Logger.getLogger(CopyOfCrmSessionFilter.class);
	
/*	
    // Filter中不能使用此方式来获取Bean,因为filter依赖于servlet,此时Servlet还未加载
    @Autowired
	RedisJpaTemplateZSet opsForZSet;
	@Autowired
	RedisJpaTemplateString rTemplate;*/
	
	/**
	 * prefix 缓存前缀(用于区分缓存,防止缓存键值重复) 在用户登录信息存入redis时使用
	 */
	private final String prefix = RedisCodes.MERCHANT_PREFIX_LOGIN;
	
	/**
	 * session过期时间(单位:秒)
	 */
	private final int expireTime = RedisCodes.SESSION_EXPIRE_TIME;
	
	public void destroy() {}

	public void doFilter(ServletRequest arg0, ServletResponse arg1,
			FilterChain arg2) throws IOException, ServletException {

		logger.info("开始执行方法:判断session是否超时或还未登陆系统,如果超时或还未登陆系统跳转到登陆页面");
		HttpServletRequest request = (HttpServletRequest) arg0;
		HttpServletResponse response = (HttpServletResponse) arg1;
		
		RedisJpaTemplateZSet  opsForZSet = wac.getBean(RedisJpaTemplateZSet.class);
		RedisJpaTemplateString rTemplate = wac.getBean(RedisJpaTemplateString.class);
		HttpSession session = request.getSession();
				
				String sessionId = session.getId();
				String merchantLoginName = (String) session.getAttribute("merchantLoginName");

				// 把相关信息存入redis
				this.saveRedisCache(opsForZSet, rTemplate, sessionId, merchantLoginName);

				// 判断String里面的session是否过期(是否存在)
				boolean flag = rTemplate.exists(prefix,sessionId);
				
				if (flag) { // 未过期
					// 刷新过期时间
					rTemplate .setOrRefurbishExpireTime(prefix, sessionId, expireTime);
					opsForZSet.setOrRefurbishExpireTime(prefix, merchantLoginName, expireTime);
					arg2.doFilter(request, response);
					
				}else{ // 已过期
					logger.info("session过期,清空redis ZSet中的相应信息");
					opsForZSet.remove(prefix, merchantLoginName, sessionId);
				}	
	}
	public void init(FilterConfig arg0) throws ServletException {
		ServletContext context = arg0.getServletContext(); 
		wac = WebApplicationContextUtils .getWebApplicationContext(context); 
		
	}

	/**
	 * 把用户登录信息(sessionID和userName)存入redis中
	 * @param sessionId  session唯一标识
	 * @param userName 登录用户名
	 */
	public void saveRedisCache(RedisJpaTemplateZSet opsForZSet,RedisJpaTemplateString rTemplate,String sessionId,String userName) {
		logger.info("把用户登录信息(sessionID和userName)存入redis中开始     sessionId:"+sessionId+" userName:"+userName);
				
		long count = opsForZSet.size(prefix, userName);
		logger.info("登录前查询正在登录中的个数:"+count);
		
		// 如果大于等于 5个,先清除第一个再插入新登录的,否则直接插入新登录的数据信息
		if (count >= 5) {
			Set set = opsForZSet.range(prefix, userName, 0, 0);
			// 第一步:删除zset里面登录时间最早的信息
			opsForZSet.removeRange(prefix, userName, 0, 0);
			// 第二步:删除string里面登录时间最早的信息
			for (String zsetVal : set) {
				rTemplate.deleteWithPrefix(prefix, zsetVal);
			}
			// 第三步: sessionId 和 userName存放到redis的string里面(新登录的信息)
			rTemplate.setWithExpireTime(prefix, sessionId, userName,expireTime);
			// 第四步: 登录状态存储到redis的zSet(新登录的信息)
			opsForZSet.add(prefix, userName, sessionId,System.currentTimeMillis());
			// 第五步:设置zSet的过期时间
			opsForZSet.setOrRefurbishExpireTime(prefix, userName, expireTime);
			
		} else {
			rTemplate.setWithExpireTime(prefix, sessionId, userName,expireTime);
			opsForZSet.add(prefix, userName, sessionId,System.currentTimeMillis());
			opsForZSet.setOrRefurbishExpireTime(prefix, userName, expireTime);
		}
		logger.info("把用户登录信息(sessionID和userName)存入redis中结束。  ");
	}
}

 

第三步、用户点击退出登录后执行的方法

      在清空session的地方调用以下方法用于清空redis里面的存储信息

 /**
     * 用户退出登录删除redis中的session信息
     * @param sessionId session唯一标识
     * @param userName 用户登录名
     */
    public void deleteRedisCache(String sessionId,String userName) {
        log.info("用户退出登录,清空redis中相应的存储信息开始     sessionId:"+sessionId+" userName:"+userName);

        // 存储前缀,可以自己定义 MERCHANT_PREFIX_LOGIN = "LOGIN_"
        final String prefix = RedisCodes.MERCHANT_PREFIX_LOGIN;
        rTemplate.deleteWithPrefix(prefix,sessionId);// 删除string里面的信息
        opsForZSet.remove(prefix, userName, sessionId);//删除zset里面的信息
        log.info("用户退出登录,清空redis中相应的存储信息结束");
    }

因为我们设置了session过期时间,所以,就算用户不点击退出,到期后redis将自动清空过期信息。

注意:

因为过滤器依赖与servlet容器,我们看一下web.xml标签的加载顺序:context-param -> listener -> filter -> servlet ,也就是说,filter执行的时候servlet还未加载,所以filter里面不能获取IOC容器里面的Bean,所以文中就使用如下方法来获取上下文信息:

private ApplicationContext wac;

// 通过Spring提供的工具类获取ApplicationContext对象
public void init(FilterConfig arg0) throws ServletException {
		ServletContext context = arg0.getServletContext(); 
		wac = WebApplicationContextUtils .getWebApplicationContext(context); 
		
	}

		
public void doFilter(ServletRequest arg0, ServletResponse arg1,
			FilterChain arg2) throws IOException, ServletException {
        RedisJpaTemplateZSet  opsForZSet = wac.getBean(RedisJpaTemplateZSet.class);
		RedisJpaTemplateString rTemplate = wac.getBean(RedisJpaTemplateString.class);
}

 

你可能感兴趣的:(redis)