前段时间做了一个市场推广相关的项目,安全框架使用的是Shiro,缓存框架使用的是spring-data-redis。为了使用户7x24小时访问,决定把项目由单机升级为分布式部署架构。但是安全框架shiro只有单机存储的SessionDao,尽管Shrio有基于Ehcache-rmi的组播/广播实现,然而集群的分布往往是跨网段的,甚至是跨地域的,所以寻求新的方案。
运行环境Nginx + Tomcat7(3台) + JDK1.7
项目架构图
pom.xml引入配置(版本自行更换):
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.10.RELEASE</version>
</dependency>
redis.properties配置:
#============================#
#===== redis sttings ====#
#============================#
redis.host=127.0.0.1
redis.port=6379
redis.password=123456
#单位秒
redis.expire=1800
redis.timeout=2000
redis.usepool=true
redis.database=1
spring-context-redis.xml配置:
<!-- redis 配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig" />
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.host}" />
<property name="port" value="${redis.port}" />
<property name="password" value="${redis.password}" />
<property name="timeout" value="${redis.timeout}" />
<property name="poolConfig" ref="jedisPoolConfig" />
<property name="usePool" value="true" />
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
RedisSessionDAO配置(重写 AbstractSessionDAO):
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
* 重写 AbstractSessionDAO
* 使用Redis缓存
* 创建者 小柒2012
* 创建时间 2018年1月10日
*/
public class RedisSessionDAO extends AbstractSessionDAO {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
/**
* shiro-redis的session对象前缀
*/
private RedisTemplate<String, Object> redisTemplate;
// 0 - never expire
private int expire = 3600000;
/**
* The Redis key prefix for the sessions
*/
private String keyPrefix = "shiro_market_redis_session:";
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
/**
* save session
* @param session
* @throws UnknownSessionException
*/
private void saveSession(Session session) throws UnknownSessionException{
if(session == null || session.getId() == null){
logger.error("session or session id is null");
return;
}
String key = session.getId().toString();
session.setTimeout(expire);
redisTemplate.opsForValue().set(keyPrefix+key, session, expire, TimeUnit.MILLISECONDS);
}
@Override
public void delete(Session session) {
if(session == null || session.getId() == null){
logger.error("session or session id is null");
return;
}
redisTemplate.delete(keyPrefix+session.getId().toString());
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<String> keys = redisTemplate.keys(this.keyPrefix + "*");
if(keys != null && keys.size()>0){
for(String key:keys){
Session s = (Session)redisTemplate.opsForValue().get(key);
sessions.add(s);
}
}
return sessions;
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
logger.error("session id is null");
return null;
}
Session s = (Session)redisTemplate.opsForValue().get(keyPrefix+sessionId);
return s;
}
/**
* Returns the Redis session keys
* prefix.
* @return The prefix
*/
public String getKeyPrefix() {
return keyPrefix;
}
/**
* Sets the Redis sessions key
* prefix.
* @param keyPrefix The prefix
*/
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
spring-shiro.xml配置:
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 会话超时时间,单位:毫秒 20m=1200000ms, 30m=1800000ms, 60m=3600000ms-->
<!-- 设置session过期时间为1小时(单位:毫秒),默认为30分钟 -->
<!-- 如果设置 Redis缓存 此处不生效将 -->
<property name="globalSessionTimeout" value="3600000"></property>
<property name="sessionValidationSchedulerEnabled" value="true"></property>
<property name="sessionIdUrlRewritingEnabled" value="false"></property>
<!-- 注入 redisSessionDAO -->
<property name="sessionDAO" ref="sessionDAO"/>
</bean>
<!-- redisSessionDAO -->
<bean id="sessionDAO" class="com.acts.market.common.session.RedisSessionDAO">
<property name="redisTemplate" ref="redisTemplate" />
</bean>
乱码问题
2018年1月11日,新增了一个在线用户查询的功能,使用API查询所有用户:
Collection<Session> sessions = redisSessionDAO.getActiveSessions();
结果sessions的size居然为空,继续跟踪底层代码:
private String keyPrefix = "shiro_market_redis_session:";
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<Serializable> keys = redisTemplate.keys(this.keyPrefix + "*");
if(keys != null && keys.size()>0){
for(Serializable key:keys){
Session s = (Session)redisTemplate.opsForValue().get(key);
sessions.add(s);
}
}
return sessions;
}
感觉API没啥问题,后台登录redis查询下:
./redis-cli -h 192.168.1.180
# 输入 auth password (没有设置密码的略过)
查看所有Keys:
keys *
keys中居然出现了乱码
由于之前是精确匹配,虽然也有乱码的问题,但是可以查询出来,这次模糊匹配就出问题了。
由于我们使用的是spring-data-redis 中的核心操作类是 RedisTemplate<K, V>, key 和 value 都是泛型的,这就涉及到将类型进行序列化的问题了。
RedisTemplate源码中存在以下序列环工具类:
private RedisSerializer<?> defaultSerializer;
private ClassLoader classLoader;
private RedisSerializer keySerializer = null;
private RedisSerializer valueSerializer = null;
private RedisSerializer hashKeySerializer = null;
private RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = new StringRedisSerializer();
默认使用的是:
if (defaultSerializer == null) {
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
继续跟踪JdkSerializationRedisSerializer中的序列化方法:
public byte[] serialize(Object object) {
if (object == null) {
return SerializationUtils.EMPTY_ARRAY;
}
try {
return serializer.convert(object);
} catch (Exception ex) {
throw new SerializationException("Cannot serialize", ex);
}
}
SerializingConverter 类中的转化方法:
/**
* Serializes the source object and returns the byte array result.
*/
@Override
public byte[] convert(Object source) {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
try {
this.serializer.serialize(source, byteStream);
return byteStream.toByteArray();
}
catch (Throwable ex) {
throw new SerializationFailedException("Failed to serialize object using " +
this.serializer.getClass().getSimpleName(), ex);
}
}
由于项目中使用String作为缓存的key,变更了序列化类就可以了。
解决办法:
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
p:connection-factory-ref="jedisConnectionFactory">
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
</bean>
作者: 小柒2012
分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
热门评论
你好,按照你的代码写了一遍。问题是:每次访问登录页,还没登录,session就存入redis了。找不出原因