一、前言
前面的文章中,shiro使用的是ehcache做缓存,这样在单机服务中,没有任何问题,但是如果是在集群环境下,就无法实现session共享了。分布式session有多种实现方式:
1. Session Replication 方式管理 (即session复制)
简介:将一台机器上的Session数据广播复制到集群中其余机器上
使用场景:机器较少,网络流量较小
优点:实现简单、配置较少、当网络中有机器Down掉时不影响用户访问
缺点:广播式复制到其余机器有一定廷时,带来一定网络开销
2. Session Sticky 方式管理
简介:即粘性Session、当用户访问集群中某台机器后,强制指定后续所有请求均落到此机器上
使用场景:机器数适中、对稳定性要求不是非常苛刻
优点:实现简单、配置方便、没有额外网络开销
缺点:网络中有机器Down掉时、用户Session会丢失、容易造成单点故障
3. 缓存集中式管理
简介:将Session存入分布式缓存集群中的某台机器上,当用户访问不同节点时先从缓存中拿Session信息
使用场景:集群中机器数多、网络环境复杂
优点:可靠性好
缺点:实现复杂、稳定性依赖于缓存的稳定性、Session信息放入缓存时要有合理的策略写入
本文介绍第三种方式实现分布式session
二、具体实现
使用缓存集中式管理实现分布式session,必然要有一个分布式的缓存,这里采用redis实现
1.集成redis
- 引入redis相关jar包
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.1.0</version>
</dependency>
- 添加redis配置
spring-redis.xml配置
<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:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
">
<!-- scanner redis properties -->
<context:property-placeholder location="/WEB-INF/property/redis.properties" />
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="maxActive" value="${redis.maxActive}" />
<property name="maxWait" value="${redis.maxWait}" />
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
</bean>
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:host-name="${redis.host}"
p:port="${redis.port}"
p:password="${redis.pass}"
p:pool-config-ref="poolConfig"/>
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="connectionFactory" />
</bean>
</beans>
redis.properties配置
redis.host=127.0.0.1
redis.port=6379
redis.pass=
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true
2.重写session
之前的session是通过EnterpriseCacheSessionDAO来生成,查询的,那么如果想重写session,必然也是重写session的增删改查,所以只需要继承EnterpriseCacheSessionDAO类来,重写有关session的方法即可。
package com.wangcongming.crm.config.shiro;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;
/**
*
* @ClassName: RedisSessionDAO
* @Description:redis实现共享session
* @author: wangcongming
* @date: 2018年6月28日 下午3:04:41
*
*/
@Repository
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
/*
* session 在redis过期时间是30分钟30*60
*/
private int expireTime = 1800;
//shiro session
public final static String USER_SHIRO_SESSION = "user:shiro:session:";
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String, Serializable> redisTemplate;
@Resource(name = "redisTemplate")
private ValueOperations<String, SimpleSession> vops;
/**
*
* <p>Title: doCreate</p>
* <p>Description:创建session,保存到数据库 </p>
* @param session
* @return
* @see EnterpriseCacheSessionDAO#doCreate(Session)
*/
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
logger.debug("创建session:{}", sessionId);
vops.set(USER_SHIRO_SESSION + sessionId.toString(), (SimpleSession)session);
return sessionId;
}
// 获取session
@Override
protected Session doReadSession(Serializable sessionId) {
logger.debug("获取session:{}", sessionId);
// 先从缓存中获取session,如果没有再去数据库中获取
Session session = super.doReadSession(sessionId);
if (session == null) {
session = (SimpleSession)vops.get(USER_SHIRO_SESSION + sessionId.toString());
}
return session;
}
/**
*
* <p>Title: doUpdate</p>
* <p>Description: 更新session的最后一次访问时间</p>
* @param session
* @see EnterpriseCacheSessionDAO#doUpdate(Session)
*/
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
logger.debug("获取session:{}", session.getId());
String key = USER_SHIRO_SESSION + session.getId().toString();
if (!redisTemplate.hasKey(key)) {
vops.set(key, (SimpleSession)session);
}
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
}
/**
*
* <p>Title: doDelete</p>
* <p>Description: 删除session</p>
* @param session
* @see EnterpriseCacheSessionDAO#doDelete(Session)
*/
@Override
protected void doDelete(Session session) {
super.doDelete(session);
logger.debug("删除session:{}", session.getId());
redisTemplate.delete(USER_SHIRO_SESSION + session.getId().toString());
}
}
在以上实现中可以看到,都是先调用EnterpriseCacheSessionDAO的方法进行一个本地session的操作,然后再去redis中操作session,这样做就可以减少连接redis的次数,效率更高。
3.重写cache
shiro中有缓存策略,之前使用的ehcache,ehcache是一种本地缓存,不适用于分布式服务,对缓存有研究的人,应该都知道,缓存分为存储缓存的cache和管理缓存的CacheManager,如果想实现分布式缓存,只需要重写cache和CacheManager即可。
- cache重写
package com.wangcongming.crm.config.shiro;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
public class ShiroRedisCache<K, V> implements Cache<K, V> {
private String cacheKey;
private RedisTemplate<K, V> redisTemplate;
//过期时间
private long globExpire = 30;
private final static String USER_SHIRO_PREFLX = "user:shiro:preflx:";
@SuppressWarnings("rawtypes")
public ShiroRedisCache(String name, RedisTemplate client) {
this.cacheKey = String.format("%s%s:", USER_SHIRO_PREFLX,name);
this.redisTemplate = client;
}
@Override
public V get(K key) throws CacheException {
redisTemplate.boundValueOps(getCacheKey(key)).expire(globExpire, TimeUnit.MINUTES);
return redisTemplate.boundValueOps(getCacheKey(key)).get();
}
@Override
public V put(K key, V value) throws CacheException {
V old = get(key);
redisTemplate.boundValueOps(getCacheKey(key)).set(value);
return old;
}
@Override
public V remove(K key) throws CacheException {
V old = get(key);
redisTemplate.delete(getCacheKey(key));
return old;
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(keys());
}
@Override
public int size() {
return keys().size();
}
@Override
public Set<K> keys() {
return redisTemplate.keys(getCacheKey("*"));
}
@Override
public Collection<V> values() {
Set<K> set = keys();
List<V> list = new ArrayList<>();
for (K s : set) {
list.add(get(s));
}
return list;
}
private K getCacheKey(Object k) {
return (K) (this.cacheKey + k);
}
}
看代码就可以看出,重写cache就是实现shiro定义的org.apache.shiro.cache.Cache接口即可。
- 重写CacheManager
package com.wangcongming.crm.config.shiro;
import java.io.Serializable;
import javax.annotation.Resource;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
/**
*
* @ClassName: RedisCacheManager
* @Description:使用redis实现shiro缓存管理
* @author: wangcongming
* @date: 2018年2月1日 下午3:05:07
*
*/
public class RedisCacheManager implements CacheManager {
@Resource
private RedisTemplate<String, Serializable> redisTemplate;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return new ShiroRedisCache<K, V>(name, redisTemplate);
}
public RedisTemplate<String, Serializable> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<String, Serializable> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
同样的重写cacheManager也是实现shiro定义的org.apache.shiro.cache.CacheManager接口即可
4.配置
实现了session和cache的重写,但是此时还没有被spring shiro使用,所以需要通过配置来实现让容器使用自己实现的session和cache,具体实现如下:
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"></bean>
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
</bean>
<!-- 管理Session -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionDAO" ref="sessionDAO"/>
<property name="cacheManager" ref="cacheManager" />
</bean>