目前安全框架shiro使用较为广泛,其功能也比较强大。为了分布式session共享,通常的做法是将session存储在redis中,实现多个节点获取同一个session。此实现可以实现session共享,但session的特点是内存存储,就是为了高速频繁访问,每个请求都必须验证session是否存在是否过期,也从session中获取数据。这样导致一个页面刷新过程中的数十个请求会同时访问redis,在几毫秒内同时操作session的获取,修改,更新,保存,删除等操作,从而造成redis的并发量飙升,刷新一个页面操作redis几十到几百次。
为了解决由于session共享造成的redis高并发问题,很明显需要在redis之前做一次短暂的session缓存,如果该缓存存在就不用从redis中获取,从而减少同时访问redis的次数。如果做session缓存,主要有两种种方案,其实原理都相同:
1>重写sessionManager的retrieveSession方法。首先从request中获取session,如果request中不存在再走原来的从redis中获取。这样可以让一个请求的多次访问redis问题得到解决,因为request的生命周期为浏览器发送一个请求到接收服务器的一次响应完成,因此,在一次请求中,request中的session是一直存在的,并且不用担心session超时过期等的问题。这样就可以达到有多少次请求就几乎有多少次访问redis,大大减少单次请求,频繁访问redis的问题。大大减少redis的并发数量。此实现方法最为简单。
1 packagecn.uce.web.login.filter;2
3 importjava.io.Serializable;4
5 importjavax.servlet.ServletRequest;6
7 importorg.apache.shiro.session.Session;8 importorg.apache.shiro.session.UnknownSessionException;9 importorg.apache.shiro.session.mgt.SessionKey;10 importorg.apache.shiro.web.session.mgt.DefaultWebSessionManager;11 importorg.apache.shiro.web.session.mgt.WebSessionKey;12
13 public class ShiroSessionManager extendsDefaultWebSessionManager {14 /**
15 * 获取session16 * 优化单次请求需要多次访问redis的问题17 *@paramsessionKey18 *@return
19 *@throwsUnknownSessionException20 */
21 @Override22 protected Session retrieveSession(SessionKey sessionKey) throwsUnknownSessionException {23 Serializable sessionId =getSessionId(sessionKey);24
25 ServletRequest request = null;26 if (sessionKey instanceofWebSessionKey) {27 request =((WebSessionKey) sessionKey).getServletRequest();28 }29
30 if (request != null && null !=sessionId) {31 Object sessionObj =request.getAttribute(sessionId.toString());32 if (sessionObj != null) {33 return(Session) sessionObj;34 }35 }36
37 Session session = super.retrieveSession(sessionKey);38 if (request != null && null !=sessionId) {39 request.setAttribute(sessionId.toString(), session);40 }41 returnsession;42 }43 }
2>session缓存于本地内存中。自定义cacheRedisSessionDao,该sessionDao中一方面注入cacheManager用于session缓存,另一方面注入redisManager用于session存储,当createSession和updateSession直接使用redisManager操作redis.保存session.当readSession先用cacheManager从cache中读取,如果不存在再用redisManager从redis中读取。注意:该方法最大的特点是session缓存的存活时间必须小于redis中session的存活时间,就是当redus的session死亡,cahe中的session一定死亡,为了保证这一特点,cache中的session的存活时间应该设置为s级,设置为1s比较合适,并且存活时间固定不能刷新,不能随着访问而延长存活。
/****/
packagecom.uc56.web.omg.authentication;importjava.io.Serializable;importjava.util.Collection;importjava.util.Date;importjava.util.HashSet;importjava.util.Set;importorg.apache.shiro.session.ExpiredSessionException;importorg.apache.shiro.session.Session;importorg.apache.shiro.session.UnknownSessionException;importorg.apache.shiro.session.mgt.ValidatingSession;importorg.apache.shiro.session.mgt.eis.CachingSessionDAO;importorg.apache.shiro.subject.support.DefaultSubjectContext;importorg.crazycake.shiro.SerializeUtils;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importcom.uc56.web.omg.shiroredis.CustomRedisManager;/*** 将从redis读取的session进行本地缓存,本地缓存失效时重新从redis读取并更新最后访问时间,解决shiro频繁读取redis问题*/
public class CachingShiroSessionDao extendsCachingSessionDAO {private static final Logger logger = LoggerFactory.getLogger(CachingShiroSessionDao.class);/**保存到Redis中key的前缀*/
private String keyPrefix = "";/*** jedis 操作redis的封装*/
privateCustomRedisManager redisManager;/*** 如DefaultSessionManager在创建完session后会调用该方法;
* 如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;
* 返回会话ID;主要此处返回的ID.equals(session.getId());*/@OverrideprotectedSerializable doCreate(Session session) {//创建一个Id并设置给Session
Serializable sessionId = this.generateSessionId(session);
assignSessionId(session, sessionId);this.saveSession(session);returnsessionId;
}/*** 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读*/@Overridepublic Session readSession(Serializable sessionId) throwsUnknownSessionException {
Session session=getCachedSession(sessionId);if (session == null
|| session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
session= this.doReadSession(sessionId);if (session == null) {throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
}else{//缓存
cache(session, session.getId());
}
}returnsession;
}/*** 根据会话ID获取会话
*
*@paramsessionId 会话ID
*@return
*/@OverrideprotectedSession doReadSession(Serializable sessionId) {
ShiroSession shiroSession= null;try{
shiroSession= (ShiroSession)SerializeUtils.deserialize(redisManager.get(this.getByteKey(sessionId)));if (shiroSession != null
&& shiroSession.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) != null) {//检查session是否过期
shiroSession.validate();//重置Redis中Session的最后访问时间
shiroSession.setLastAccessTime(newDate());this.saveSession(shiroSession);
logger.info("sessionId {} name {} 被读取并更新访问时间", sessionId, shiroSession.getClass().getName());
}
}catch(Exception e) {if (!(e instanceofExpiredSessionException)) {
logger.warn("读取Session失败", e);
}else{
logger.warn("session已失效:{}", e.getMessage());
}
}returnshiroSession;
}//扩展更新缓存机制,每次请求不重新更新session,更新session会延长session的失效时间
@Overridepublic void update(Session session) throwsUnknownSessionException {
doUpdate(session);if (session instanceofValidatingSession) {if(((ValidatingSession) session).isValid()) {//不更新ehcach中的session,使它在设定的时间内过期//cache(session, session.getId());
} else{
uncache(session);
}
}else{
cache(session, session.getId());
}
}/*** 更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用*/@Overrideprotected voiddoUpdate(Session session) {//如果会话过期/停止 没必要再更新了
try{if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {return;
}
}catch(Exception e) {
logger.error("ValidatingSession error");
}try{if (session instanceofShiroSession) {//如果没有主要字段(除lastAccessTime以外其他字段)发生改变
ShiroSession shiroSession =(ShiroSession) session;if (!shiroSession.isChanged()) {return;
}
shiroSession.setChanged(false);this.saveSession(session);
logger.info("sessionId {} name {} 被更新", session.getId(), session.getClass().getName());
}else if (session instanceofSerializable) {this.saveSession(session);
logger.info("sessionId {} name {} 作为非ShiroSession对象被更新, ", session.getId(), session.getClass().getName());
}else{
logger.warn("sessionId {} name {} 不能被序列化 更新失败", session.getId(), session.getClass().getName());
}
}catch(Exception e) {
logger.warn("更新Session失败", e);
}
}/*** 删除会话;当会话过期/会话停止(如用户退出时)会调用*/@Overrideprotected voiddoDelete(Session session) {try{
redisManager.del(this.getByteKey(session.getId()));
logger.debug("Session {} 被删除", session.getId());
}catch(Exception e) {
logger.warn("修改Session失败", e);
}
}/*** 删除cache中缓存的Session*/
public voiduncache(Serializable sessionId) {
Session session= this.readSession(sessionId);super.uncache(session);
logger.info("取消session {} 的缓存", sessionId);
}/***
* 统计当前活动的session*/@Overridepublic CollectiongetActiveSessions() {
Set sessions = new HashSet();
Set keys = redisManager.keys(this.keyPrefix + "*");if(keys != null && keys.size()>0){for(byte[] key:keys){
Session s=(Session)SerializeUtils.deserialize(redisManager.get(key));
sessions.add(s);
}
}returnsessions;
}/*** save session
*@paramsession
*@throwsUnknownSessionException*/
private void saveSession(Session session) throwsUnknownSessionException{if(session == null || session.getId() == null){
logger.error("session or session id is null");return;
}byte[] key =getByteKey(session.getId());byte[] value =SerializeUtils.serialize(session);
session.setTimeout(redisManager.getExpire()* 1L);this.redisManager.set(key, value, redisManager.getExpire());
}/*** 将key转换为byte[]
*@paramkey
*@return
*/
private byte[] getByteKey(Serializable sessionId){
String preKey= this.keyPrefix +sessionId;returnpreKey.getBytes();
}publicCustomRedisManager getRedisManager() {returnredisManager;
}public voidsetRedisManager(CustomRedisManager redisManager) {this.redisManager =redisManager;/*** 初使化RedisManager*/
this.redisManager.init();
}/*** 获取 保存到Redis中key的前缀
*@returnkeyPrefix*/
publicString getKeyPrefix() {returnkeyPrefix;
}/*** 设置 保存到Redis中key的前缀
*@paramkeyPrefix 保存到Redis中key的前缀*/
public voidsetKeyPrefix(String keyPrefix) {this.keyPrefix =keyPrefix;
}
}
/****/
packagecom.uc56.web.omg.authentication;importjava.io.Serializable;importjava.util.Date;importjava.util.Map;importorg.apache.shiro.session.mgt.SimpleSession;/*** 由于SimpleSession lastAccessTime更改后也会调用SessionDao update方法,
* 增加标识位,如果只是更新lastAccessTime SessionDao update方法直接返回*/
public class ShiroSession extends SimpleSession implementsSerializable {/****/
private static final long serialVersionUID = 1L;//除lastAccessTime以外其他字段发生改变时为true
private booleanisChanged;publicShiroSession() {super();this.setChanged(true);
}publicShiroSession(String host) {super(host);this.setChanged(true);
}
@Overridepublic voidsetId(Serializable id) {super.setId(id);this.setChanged(true);
}
@Overridepublic voidsetStopTimestamp(Date stopTimestamp) {super.setStopTimestamp(stopTimestamp);this.setChanged(true);
}
@Overridepublic void setExpired(booleanexpired) {super.setExpired(expired);this.setChanged(true);
}
@Overridepublic void setTimeout(longtimeout) {super.setTimeout(timeout);this.setChanged(true);
}
@Overridepublic voidsetHost(String host) {super.setHost(host);this.setChanged(true);
}
@Overridepublic void setAttributes(Mapattributes) {super.setAttributes(attributes);this.setChanged(true);
}
@Overridepublic voidsetAttribute(Object key, Object value) {super.setAttribute(key, value);this.setChanged(true);
}
@OverridepublicObject removeAttribute(Object key) {this.setChanged(true);return super.removeAttribute(key);
}//更新最后访问时间不更新redis
@Overridepublic voidtouch() {this.setChanged(false);super.touch();
}/*** 停止*/@Overridepublic voidstop() {super.stop();this.setChanged(true);
}/*** 设置过期*/@Overrideprotected voidexpire() {this.stop();this.setExpired(true);
}public booleanisChanged() {returnisChanged;
}public void setChanged(booleanisChanged) {this.isChanged =isChanged;
}
@Overridepublic booleanequals(Object obj) {return super.equals(obj);
}
@Overrideprotected booleanonEquals(SimpleSession ss) {return super.onEquals(ss);
}
@Overridepublic inthashCode() {return super.hashCode();
}
@OverridepublicString toString() {return super.toString();
}
}
/****/
packagecom.uc56.web.omg.authentication;importorg.apache.shiro.session.Session;importorg.apache.shiro.session.mgt.SessionContext;importorg.apache.shiro.session.mgt.SessionFactory;public class ShiroSessionFactory implementsSessionFactory {
@OverridepublicSession createSession(SessionContext initData) {
ShiroSession session= newShiroSession();returnsession;
}
}
/login/login.do=anon
/login/loginAuthc.do=anon
/login/authCheck.do=anon
/login/forbidden.do=anon
/login/validateUser.do=anon
/city/**=anon
/easyui-themes/**=anon
/images/**=anon
/jquery-easyui-1.5.1/**=anon
/scripts/**=anon
/users/**=anon
/**=LoginFailureCheck,authc,user
/>
此设计中最重要的一点就是:
1.cache中的session只存储不更新,也就是说每次访问不会刷新缓存中的session,cache中的session一定会在设定的时间中过期
2.cache中设置的session的时间一定要短于redis中存储的session,保证redis中session过期是,cache中的session一定过期
3.redis中的session更新会清楚cache中的session保证session一直性