一、频繁从Redis 中读取Session 的问题说明:针对这个问题
问题描述:
每次请求需要访问多次Redis服务,即使是同一个请求,每次也要从Redis 读取Session,
这个访问的频次会出现很长时间的IO等待,对每次请求的性能减低了,并且对Redis的压力
也提高了。
针对这个问题一般有2个方案来解决,下边分别来看下这2个方案,并对比下优劣
1、方案一
说明:该方案前边"Shiro学习(四)" 已经给出了一种解决方案,为了2中方案的对比,
现在把"Shiro学习(四)" 中的方案也拿过来
Shiro 在实际工作中常常用于Web 项目,通过我们使用的SessionManager对象
DefaultWebSessionManager 也可以确定是Web 项目;
在Web 中,在一个请求处于活动中时,ServletRequest中会缓存用户的很多信息,其中
就包括 Session 信息,那么我们可不可以先从 ServletRequest 读取用户的Session,
ServletRequest 中读取不到我们再去Redis 中读取Session
通过Debug 发现,Shiro是在 DefaultWebSessionManager.retrieveSession() 中调用
SessionDAO 的 readSession() 方法去读取Session 的。
我们可以重写 DefaultWebSessionManager.retrieveSession() 方法,先从ServletRequest 中
读取Session,ServletRequest 中没有 再去Redis 中读取,实现步骤如下:
1)自定义 SessionManager 继承 DefaultWebSessionManager,并重写 retrieveSession() 方法
java
/****************************************************
* 解决单次请求多次访问Redis 的问题
* @author lbf
* @date 2025/4/28 15:08
****************************************************/
public class ShiroSessionManager extends DefaultWebSessionManager {
private static Logger logger = LoggerFactory.getLogger(DefaultWebSessionManager.class);
/**
* 优化读取 Session
* 先从 ServletRequest 请求域中读取,ServletRequest 中不存在再去Redis 中读取
*
* @param sessionKey the session key to use to look up the target session.
* @return
* @throws UnknownSessionException
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
if (request != null && null != sessionId) {
Object sessionObj = request.getAttribute(sessionId.toString());
if (sessionObj != null) {
logger.debug("read session from request");
return (Session) sessionObj;
}
}
Session session = super.retrieveSession(sessionKey);
if (request != null && null != sessionId) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
RedisSessionDAO代码如下:
java
/****************************************************
* 自定义 SeesionDAO,用于将session 数据保存到 Redis
* 参考
* @author lbf
* @date 2025/4/23 13:45
****************************************************/
@Slf4j
//@Component
public class RedisSessionDAO extends AbstractSessionDAO {
/**
* 常量,key前缀
*/
private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:";
/**
* key前缀,可以手动指定
*/
private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX;
/**
* 超时时间
*/
private static final int DEFAULT_EXPIRE = -2;//默认的过期时间,即session.getTimeout() 中的过期时间
private static final int NO_EXPIRE = -1;//没有过期时间
/**
* 请保确session在redis中的过期时间长于sesion.getTimeout(),
* 否则会在session还没过期,redis存储session已经过期了会把session自动删除
*/
private int expire = DEFAULT_EXPIRE;
/**
* 毫秒与秒的换算单位
*/
private static final int MILLISECONDS_IN_A_SECOND = 1000;
private RedisManager redisManager;
@Override
protected Serializable doCreate(Session session) {
if(session == null){
log.error("session is null");
throw new UnknownSessionException("session is null");
}
//1、根据Seesion 生成 SeesionId
Serializable sessionId = this.generateSessionId(session);
//2、关联sessionId 与 session,可以基于sessionId拿到session
this.assignSessionId(session,sessionId);
//3、保存session
saveSession(session);
return sessionId;
}
private void saveSession(Session session){
if(session == null || session.getId() == null){
log.error("session or sessionId is null");
throw new UnknownSessionException("session or sessionId is null");
}
//获取 redis 中的 sessionKey
String redisSessionKey = getRedisSessionKey(session.getId());
if(expire == DEFAULT_EXPIRE){
this.redisManager.set(redisSessionKey,session,(int)session.getTimeout()/MILLISECONDS_IN_A_SECOND);
return;
}
if(expire != NO_EXPIRE && expire*MILLISECONDS_IN_A_SECOND < session.getTimeout()){
log.warn("Redis session expire time: "
+ (expire * MILLISECONDS_IN_A_SECOND)
+ " is less than Session timeout: "
+ session.getTimeout()
+ " . It may cause some problems.");
}
this.redisManager.set(redisSessionKey,session,expire);
}
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
log.error("sessionId is null !");
}
Session session = null;
try{
//从Redis 中读取session
String redisKey = getRedisSessionKey(sessionId);
session = (Session)redisManager.get(redisKey);
}catch (Exception e){
log.error(" get session fail sessionId = {}",sessionId);
}
return session;
}
/**
* 更新Session
* @param session the Session to update
* @throws UnknownSessionException
*/
@Override
public void update(Session session) throws UnknownSessionException {
if(session == null || session.getId() == null){
log.error(" session is null or sessionId is null");
throw new UnknownSessionException("session is null or sessionId is null");
}
//如果会话过期/停止 没必要再更新了
try {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
return;
}
this.saveSession(session);
} catch (Exception e) {
log.warn("update Session is failed", e);
}
}
@Override
public void delete(Session session) {
if(session == null || session.getId() == null){
log.error(" session is null or sessionId is null");
throw new UnknownSessionException("session is null or sessionId is null");
}
String redisSessionKey = getRedisSessionKey(session.getId());
redisManager.del(redisSessionKey);
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<>();
try{
Set<String> keySets = redisManager.scan(this.keyPrefix+"*");
for(String key:keySets){
Session session = (Session)redisManager.get(key);
sessions.add(session);
}
}catch (Exception e){
log.error(" get active session fail !",e);
}
return sessions;
}
/**
* 获取session存储在Redis 中的key
* @param sessionId
* @return
*/
public String getRedisSessionKey(Serializable sessionId){
return this.keyPrefix+sessionId;
}
public RedisManager getRedisManager() {
return redisManager;
}
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
}
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
public long getSessionInMemoryTimeout() {
return sessionInMemoryTimeout;
}
public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) {
this.sessionInMemoryTimeout = sessionInMemoryTimeout;
}
public int getExpire() {
return expire;
}
public void setExpire(int expire) {
this.expire = expire;
}
}
2)在Shiro 配置类 ShiroConfig 用自定义的 SessionManager 替代 DefaultWebSessionManager
java
@Bean
public SessionManager sessionManager(){
//DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
ShiroSessionManager sessionManager = new ShiroSessionManager();
//设置session过期时间为1小时(单位:毫秒),默认为30分钟
sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setDeleteInvalidSessions(true);
//注入会话监听器
sessionManager.setSessionListeners(Collections.singleton(new MySessionListener1()));
//可以不配置,默认有实现
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("BASIC_WEBSID");
simpleCookie.setHttpOnly(true);
sessionManager.setSessionIdCookie(simpleCookie);
//设置SessionDAO
//todo 在配置类中Bean的另一种注入方式
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
2、方案二
参考开源项目 shiro-redis,使用ThreadLocal 来缓存Session,每次RedisSessionDAO读取
Session 时,先从ThrealLocal 中读取,若 ThreadLocal 中的Session 不存在或已经过期,则
再去Redis 中读取,实现步骤如下:
1)因为ThreadLocal 只能存储对象,而我们需要判断Session是否过期,所以需要对Session包
装一下,不能直接存储;
自定义用于存储Session到 ThreadLocal 的对象 SessionInMemory,代码如下:
java
/****************************************************
* 参考开源项目 shiro-redis 解决频繁从redis 中读取session 的问题
*
* 该类是为了方便存储带有超时时间的Session
*
* @author lbf
* @date 2025/4/28 10:22
****************************************************/
public class SessionInMemory {
/**
* Session
*/
private Session session;
/**
* 创建时间
*/
private Date createTime;
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
2)修改 RedisSessionDAO 中的 doReadSession() 方法
java
//添加的字段
private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 5000L;
/*
* session存储在内存的时间
*/
private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT;
private static ThreadLocal sessionsInThread = new ThreadLocal();
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
log.error("sessionId is null !");
}
Session session = null;
try{
//1、先从内存中读取Session,即从 ThreadLocal 中读取Session
session = getSessionFromThreadLocal(sessionId);
if(session != null){
return session;
}
//2、ThreadLocal中Session 不存在,然后再从Redis 中读取session
String redisKey = getRedisSessionKey(sessionId);
session = (Session)redisManager.get(redisKey);
if(session != null){
//将session 保存到内存ThreadLocal 中
setSessionToThreadLocal(sessionId,session);
}
}catch (Exception e){
log.error(" get session fail sessionId = {}",sessionId);
}
return session;
}
/**
* 基于 ThreadLocal 缓存一个请求的Session,避免频繁的从Redis 中读取Session
*
* @param sessionId
* @return
*/
private Session getSessionFromThreadLocal(Serializable sessionId){
Session session = null;
if(sessionsInThread.get() == null){
return null;
}
Map<Serializable,SessionInMemory> memoryMap = (Map<Serializable,SessionInMemory>)sessionsInThread.get();
SessionInMemory sessionInMemory = memoryMap.get(sessionId);
if(sessionInMemory == null){
return null;
}
//若Session存在于ThreadLocal 中,则判断Session 是否过期
Date now = new Date();
long duration = now.getTime() - sessionInMemory.getCreateTime().getTime();
if(duration <= MILLISECONDS_IN_A_SECOND){//未过期
session = sessionInMemory.getSession();
log.debug("read session from memory");
}else {
//Session 在ThreadLocal 过期了,则删除
memoryMap.remove(sessionId);
}
return session;
}
private void setSessionToThreadLocal(Serializable sessionId,Session session){
Map<Serializable,SessionInMemory> memoryMap = (Map<Serializable,SessionInMemory>)sessionsInThread.get();
if(memoryMap == null){
memoryMap = new ConcurrentHashMap<>();
//将 memoryMap 存放到 ThreadLocal 中
sessionsInThread.set(memoryMap);
}
//构建SessionInMemory
SessionInMemory sessionInMemory = new SessionInMemory();
sessionInMemory.setSession(session);
sessionInMemory.setCreateTime(new Date());
memoryMap.put(sessionId,sessionInMemory);
}
3)配置类ShiroConfig中 SessionManager 使用默认的 DefaultWebSessionManager
java
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间为1小时(单位:毫秒),默认为30分钟
sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setDeleteInvalidSessions(true);
//注入会话监听器
sessionManager.setSessionListeners(Collections.singleton(new MySessionListener1()));
//可以不配置,默认有实现
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("BASIC_WEBSID");
simpleCookie.setHttpOnly(true);
sessionManager.setSessionIdCookie(simpleCookie);
//设置SessionDAO
//todo 在配置类中Bean的另一种注入方式
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
二、频繁的将Session 更新到Redis的问题
问题描述:
通过Debug 发现,Shiro默认创建的Session是 SimpleSession,且每次访问Session
后,SimpleSession 的 lastAccessTime(最后访问时间) 就会发生改变,lastAccessTime
改变会触发将Session 更新到 Redis,如下图所示:



针对该问题有3种解决方案,下边分别看下这3种方案:
1、方案一
参考开源项目shiro-redis,新定义一个ShiroSession 并继承 SimpleSession,在 ShiroSession
中定义一个标志位 isChange,只有 lastAccessTime 之外的属性发生改变时,isChange被设置
为 True,只有 isChange 发生改变时 isChange 被设置为false;然后在RedisSessionDAO中
的update() 方法中先判断标志位 isChange 的值,isChange=true才执行更新操作,
实现步骤如下:
1)定义ShiroSession
java
/****************************************************
* 在前边 8001 模块中存在一个问题,即:
* 每次操作系统时,SimpleSession(Session 的实现)的 lastAccessTime(最后一次被访问时间)就会被改变,
* 此时就会触发 SessionDAO.update 去更新Redis 中的Session,会频繁的向Redis 写数据
*
* 针对这个问题,我们自定义Session(如:ShiroSession),继承 SimpleSession;在 ShiroSession 中
* 添加标志位属性 isChanged,除了 lastAccessTime 之外的字段发生改变时,isChanged 被设置为true,
* 否则 isChanged 被设置为false;然后在 RedisSessionDAO 的update() 方法中先判断 isChanged
* 的值是否为true,isChanged==true才执行更新操作(参考开源项目 shiro-redis)
*
*
* @author lbf
* @date 2025/4/25 14:25
****************************************************/
public class ShiroSession extends SimpleSession implements Serializable {
// 除lastAccessTime以外其他字段发生改变时为true
private boolean isChanged = false;
public ShiroSession() {
super();
this.setChanged(true);
}
public ShiroSession(String host) {
super(host);
this.setChanged(true);
}
@Override
public void setId(Serializable id) {
super.setId(id);
this.setChanged(true);
}
@Override
public void setStopTimestamp(Date stopTimestamp) {
super.setStopTimestamp(stopTimestamp);
this.setChanged(true);
}
@Override
public void setExpired(boolean expired) {
super.setExpired(expired);
this.setChanged(true);
}
@Override
public void setTimeout(long timeout) {
super.setTimeout(timeout);
this.setChanged(true);
}
@Override
public void setHost(String host) {
super.setHost(host);
this.setChanged(true);
}
@Override
public void setAttributes(Map<Object, Object> attributes) {
super.setAttributes(attributes);
this.setChanged(true);
}
@Override
public void setAttribute(Object key, Object value) {
super.setAttribute(key, value);
this.setChanged(true);
}
@Override
public Object removeAttribute(Object key) {
this.setChanged(true);
return super.removeAttribute(key);
}
/**
* 停止
*/
@Override
public void stop() {
super.stop();
this.setChanged(true);
}
/**
* 设置过期
*/
@Override
protected void expire() {
this.stop();
this.setExpired(true);
}
public boolean isChanged() {
return isChanged;
}
public void setChanged(boolean isChanged) {
this.isChanged = isChanged;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
@Override
protected boolean onEquals(SimpleSession ss) {
return super.onEquals(ss);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public String toString() {
return super.toString();
}
}
2)修改 RedisSessionDAO 中 的 update()
java
@Override
public void update(Session session) throws UnknownSessionException {
if(session == null || session.getId() == null){
log.error(" session is null or sessionId is null");
throw new UnknownSessionException("session is null or sessionId is null");
}
//如果会话过期/停止 没必要再更新了
try {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
return;
}
if (session instanceof ShiroSession) {
// 如果没有主要字段(除lastAccessTime以外其他字段)发生改变
ShiroSession ss = (ShiroSession) session;
if (!ss.isChanged()) {
return;
}
//如果没有返回 证明有调用 setAttribute往redis 放的时候永远设置为false
ss.setChanged(false);
}
this.saveSession(session);
} catch (Exception e) {
log.warn("update Session is failed", e);
}
}
3)模仿 SimpleSessionFactory 创建 ShiroSessionFactory,用于创建 ShiroSession
java
/****************************************************
* 仿造 SimpleSessionFactory 创建 ShiroSessionFactory 用于创建 ShiroSession
* 将 ShiroSessionFactory 注入到 SessionManager 才能生效
*
* @author lbf
* @date 2025/4/25 16:29
****************************************************/
public class ShiroSessionFactory implements SessionFactory {
/**
* 创建 ShiroSession
* @param initData the initialization data to be used during {@link Session} creation.
* @return
*/
@Override
public Session createSession(SessionContext initData) {
if (initData != null) {
String host = initData.getHost();
if (host != null) {
return new ShiroSession(host);
}
}
return new ShiroSession();
}
}
4)将 SimpleSessionFactory 注入到 SessionManager 中,
java
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间为1小时(单位:毫秒),默认为30分钟
sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setDeleteInvalidSessions(true);
//注入会话监听器
sessionManager.setSessionListeners(Collections.singleton(new MySessionListener1()));
//可以不配置,默认有实现
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("BASIC_WEBSID");
simpleCookie.setHttpOnly(true);
sessionManager.setSessionIdCookie(simpleCookie);
//设置SessionFactory
sessionManager.setSessionFactory(new ShiroSessionFactory());
//设置SessionDAO
//todo 在配置类中Bean的另一种注入方式
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
/**
* 使用自定义的 RedisSessionDAO,用于将session存储到Redis 中
*
* @return
*/
@Bean("sessionDAO")
public SessionDAO sessionDAO(){
RedisSessionDAO sessionDAO = new RedisSessionDAO();
RedisManager redisManager = redisManager();
sessionDAO.setRedisManager(redisManager);
return sessionDAO;
}
2、方案二
上边的"方案一"存在一个问题,即:lastAccessTime 发生改变一直不更新Redis,可能会导
致用户在线,但Redis 中的Session已经过期的问题;
针对这个问题,我们可以采用定时更新的方案,即Session 任何属性字段发生改变时,不立即
更新,而是每隔一段时间批量更新一次,实现步骤如下:
1)定义基于批量更新Session 的类 BufferUpdateRedisSessionDAO,继承 RedisSessionDAO
并重写 update 方法,实现代码如下:
java
/****************************************************
* 在前边 8001 模块中存在一个问题,即:
* 每次操作系统时,SimpleSession(Session 的实现)的 lastAccessTime(最后一次被访问时间)就会被改变,
* 此时就会触发 SessionDAO.update 去更新Redis 中的Session,会频繁的向Redis 写数据,会导致一些性能问题。
*
* 针对这个问题网上解决方案有很多,下边列出几个常用的方案:
* 1)关闭 lastAccessTime 的自动更新,sessionManager.setUpdateLastAccessEnabled(false); // 默认已开启
* 但有些版本的 DefaultWebSessionManager 已经没有这个属性了
* 2)自定义Session,如 ShiroSession ,继承 SimpleSession,如 ShiroSession ,并定义一个boolean类型标志位,如 isChange,
* 该标志位初始值是 false,只有除了lastAccessTime 之外的字段发生改变时,isChanged 被设置为true,
* 3)批量更新Session到Redis,不用每次 Session发生改变时(包括 lastAccessTime 发生改变)就立即更新Session,而是
* 每隔一段时间(如:每隔5s)更新一次Session 到 Redis,通过 ScheduledExecutorService 来实现定时更新Session
*
* todo 注意:
* 上边方案 1 和 2 ,都可能存在在线用户的Session 与 Redis 中的Session 数据不一致的情况;
* 如果用户的Session 只有 lastAccessTime 发生改变,可能会存在 用户一直在线,但Redis 中的 Session 已经过期的情况
*
* @author lbf
* @date 2025/4/25 15:51
****************************************************/
public class BufferUpdateRedisSessionDAO extends RedisSessionDAO {
/**
* 定义基于定时任务的线程池,用于定时更新Session
* 更新 Session 到Redis 需要单线程执行,所以核心线程数为1
*/
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
/**
* 最后一次需要更新的Session,更新之后 lastSession 需要设置为空
*
* 分布式下,lastSession 需要能让其他线程看到
*/
private volatile Session lastSession = null;
@Override
public void update(Session session) throws UnknownSessionException {
this.lastSession = session;
//将更新任务提交到线程池
//设置每隔5s执行一次
/**
* 这里用lambda而不用 this.flush(),是因为 scheduler.schedule() 方法
* 第一个参数是 Runnable类型的,lambda 表达式 this::flush 会把 flush 包装到 Runnable 中,
* this::flush 相当于:
* Runnable r = new Runnable() {
* @Override
* public void run() {
* flush();
* }
* };
*/
scheduler.schedule(this::flush,1, TimeUnit.SECONDS);
}
private void flush(){
if(this.lastSession != null){
//调用父类方法,真正执行
super.update(lastSession);
this.lastSession = null;
}
}
}
RedisSessionDAO 的update 方法如下:
java
@Override
public void update(Session session) throws UnknownSessionException {
if(session == null || session.getId() == null){
log.error(" session is null or sessionId is null");
throw new UnknownSessionException("session is null or sessionId is null");
}
//如果会话过期/停止 没必要再更新了
try {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
return;
}
this.saveSession(session);
} catch (Exception e) {
log.warn("update Session is failed", e);
}
}
2)在配置类ShiroConfig 中使用 BufferUpdateRedisSessionDAO 替代 RedisSessionDAO
java
@Bean("sessionDAO")
public SessionDAO sessionDAO(){
//RedisSessionDAO sessionDAO = new RedisSessionDAO();
BufferUpdateRedisSessionDAO sessionDAO = new BufferUpdateRedisSessionDAO();
RedisManager redisManager = redisManager();
sessionDAO.setRedisManager(redisManager);
return sessionDAO;
}
3、方案三
上边方案二也存在一个问题,更新间隔时间不好把握,若时间太短,可能会增加redis的压力;
若时间太长,可能会存在SimpleSession lastAccessTime 之外的属性发生改变时,Session
没有及时更新到redis中而导致Session不一致用户认证失败的问题;
可不可以把"方案一" 与 "方案二" 整合一起,当 SimpleSession lastAccessTime 之外的属性
发生改变时立即将Session 更新到 Redis 中,否则定时批量更新,实现步骤如下
1)定义 ShiroSession,代码请参考"方案一"
2)模仿 SimpleSessionFactory 创建 ShiroSessionFactory,用于创建 ShiroSession;
代码请参考"方案一"
3)修改RedisSession 中的update 方法,代码如下:
java
public class RedisSessionDAO extends AbstractSessionDAO {
/**
* 常量,key前缀
*/
private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:";
/**
* key前缀,可以手动指定
*/
private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX;
private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 5000L;
/**
*
* session存储在内存的时间
*/
private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT;
/**
* 过期时间
*/
private static final int DEFAULT_EXPIRE = -2;//默认的过期时间,即session.getTimeout() 中的过期时间
private static final int NO_EXPIRE = -1;//没有过期时间
/**
* 请保确session在redis中的过期时间长于sesion.getTimeout(),
* 否则会在session还没过期,redis存储session已经过期了会把session自动删除
*/
private int expire = DEFAULT_EXPIRE;
/**
* 毫秒与秒的换算单位
*/
private static final int MILLISECONDS_IN_A_SECOND = 1000;
private RedisManager redisManager;
private static ThreadLocal sessionsInThread = new ThreadLocal();
@Override
protected Serializable doCreate(Session session) {
if(session == null){
log.error("session is null");
throw new UnknownSessionException("session is null");
}
//1、根据Seesion 生成 SeesionId
Serializable sessionId = this.generateSessionId(session);
//2、关联sessionId 与 session,可以基于sessionId拿到session
this.assignSessionId(session,sessionId);
//3、保存session
saveSession(session);
return sessionId;
}
private void saveSession(Session session){
if(session == null || session.getId() == null){
log.error("session or sessionId is null");
throw new UnknownSessionException("session or sessionId is null");
}
//获取 redis 中的 sessionKey
String redisSessionKey = getRedisSessionKey(session.getId());
if(expire == DEFAULT_EXPIRE){
this.redisManager.set(redisSessionKey,session,(int)session.getTimeout()/MILLISECONDS_IN_A_SECOND);
return;
}
if(expire != NO_EXPIRE && expire*MILLISECONDS_IN_A_SECOND < session.getTimeout()){
log.warn("Redis session expire time: "
+ (expire * MILLISECONDS_IN_A_SECOND)
+ " is less than Session timeout: "
+ session.getTimeout()
+ " . It may cause some problems.");
}
this.redisManager.set(redisSessionKey,session,expire);
}
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
log.error("sessionId is null !");
}
Session session = null;
try{
//1、先从内存中读取Session,即从 ThreadLocal 中读取Session
session = getSessionFromThreadLocal(sessionId);
if(session != null){
return session;
}
//2、ThreadLocal中Session 不存在,然后再从Redis 中读取session
String redisKey = getRedisSessionKey(sessionId);
session = (Session)redisManager.get(redisKey);
if(session != null){
//将session 保存到内存ThreadLocal 中
setSessionToThreadLocal(sessionId,session);
}
}catch (Exception e){
log.error(" get session fail sessionId = {}",sessionId);
}
return session;
}
/**
* 基于 ThreadLocal 缓存一个请求的Session,避免频繁的从Redis 中读取Session
*
* @param sessionId
* @return
*/
private Session getSessionFromThreadLocal(Serializable sessionId){
Session session = null;
if(sessionsInThread.get() == null){
return null;
}
Map<Serializable,SessionInMemory> memoryMap = (Map<Serializable,SessionInMemory>)sessionsInThread.get();
SessionInMemory sessionInMemory = memoryMap.get(sessionId);
if(sessionInMemory == null){
return null;
}
//若Session存在于ThreadLocal 中,则判断Session 是否过期
Date now = new Date();
long duration = now.getTime() - sessionInMemory.getCreateTime().getTime();
if(duration <= MILLISECONDS_IN_A_SECOND){//未过期
session = sessionInMemory.getSession();
log.debug("read session from memory");
}else {
//Session 在ThreadLocal 过期了,则删除
memoryMap.remove(sessionId);
}
return session;
}
private void setSessionToThreadLocal(Serializable sessionId,Session session){
Map<Serializable,SessionInMemory> memoryMap = (Map<Serializable,SessionInMemory>)sessionsInThread.get();
if(memoryMap == null){
memoryMap = new ConcurrentHashMap<>();
//将 memoryMap 存放到 ThreadLocal 中
sessionsInThread.set(memoryMap);
}
//构建SessionInMemory
SessionInMemory sessionInMemory = new SessionInMemory();
sessionInMemory.setSession(session);
sessionInMemory.setCreateTime(new Date());
memoryMap.put(sessionId,sessionInMemory);
}
/**
* 更新Session
* @param session the Session to update
* @throws UnknownSessionException
*/
@Override
public void update(Session session) throws UnknownSessionException {
if(session == null || session.getId() == null){
log.error(" session is null or sessionId is null");
throw new UnknownSessionException("session is null or sessionId is null");
}
//如果会话过期/停止 没必要再更新了
try {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
return;
}
this.saveSession(session);
} catch (Exception e) {
log.warn("update Session is failed", e);
}
}
@Override
public void delete(Session session) {
if(session == null || session.getId() == null){
log.error(" session is null or sessionId is null");
throw new UnknownSessionException("session is null or sessionId is null");
}
String redisSessionKey = getRedisSessionKey(session.getId());
redisManager.del(redisSessionKey);
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<>();
try{
Set<String> keySets = redisManager.scan(this.keyPrefix+"*");
for(String key:keySets){
Session session = (Session)redisManager.get(key);
sessions.add(session);
}
}catch (Exception e){
log.error(" get active session fail !",e);
}
return sessions;
}
/**
* 获取session存储在Redis 中的key
* @param sessionId
* @return
*/
public String getRedisSessionKey(Serializable sessionId){
return this.keyPrefix+sessionId;
}
public RedisManager getRedisManager() {
return redisManager;
}
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
}
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
public long getSessionInMemoryTimeout() {
return sessionInMemoryTimeout;
}
public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) {
this.sessionInMemoryTimeout = sessionInMemoryTimeout;
}
public int getExpire() {
return expire;
}
public void setExpire(int expire) {
this.expire = expire;
}
}
4)修改 BufferUpdateRedisSessionDAO,代码如下:
java
public class BufferUpdateRedisSessionDAO extends RedisSessionDAO {
/**
* 定义基于定时任务的线程池,用于定时更新Session
* 更新 Session 到Redis 需要单线程执行,所以核心线程数为1
*/
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
/**
* 最后一次需要更新的Session,更新之后 lastSession 需要设置为空
*
* 分布式下,lastSession 需要能让其他线程看到
*/
private volatile Session lastSession = null;
@Override
public void update(Session session) throws UnknownSessionException {
this.lastSession = session;
//将更新任务提交到线程池
//设置每隔5s执行一次
/**
* 这里用lambda而不用 this.flush(),是因为 scheduler.schedule() 方法
* 第一个参数是 Runnable类型的,lambda 表达式 this::flush 会把 flush 包装到 Runnable 中,
* this::flush 相当于:
* Runnable r = new Runnable() {
* @Override
* public void run() {
* flush();
* }
* };
*/
if(this.lastSession instanceof ShiroSession){
ShiroSession shiroSession = (ShiroSession) this.lastSession;
if(shiroSession.isChanged()){
// 除了 SimpleSession lastAccessTime 之外的字段发生改变时,立即个更新
super.update(this.lastSession);
//Session 更新后标志位归位
shiroSession.setChanged(false);
this.lastSession = null;
return;
}
}
//其他情况定时更新,每隔5s更新一次
scheduler.schedule(this::flush,1, TimeUnit.SECONDS);
}
private void flush(){
if(this.lastSession != null){
//调用父类方法,真正执行
super.update(lastSession);
this.lastSession = null;
}
}
}
5)修改配置类ShiroConfig,代码如下:
java
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间为1小时(单位:毫秒),默认为30分钟
sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setDeleteInvalidSessions(true);
//注入会话监听器
sessionManager.setSessionListeners(Collections.singleton(new MySessionListener1()));
//可以不配置,默认有实现
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("BASIC_WEBSID");
simpleCookie.setHttpOnly(true);
sessionManager.setSessionIdCookie(simpleCookie);
//设置SessionFactory
sessionManager.setSessionFactory(new ShiroSessionFactory());
//设置SessionDAO
//todo 在配置类中Bean的另一种注入方式
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
/**
* 使用自定义的 RedisSessionDAO,用于将session存储到Redis 中
*
* @return
*/
@Bean("sessionDAO")
public SessionDAO sessionDAO(){
//RedisSessionDAO sessionDAO = new RedisSessionDAO();
BufferUpdateRedisSessionDAO sessionDAO = new BufferUpdateRedisSessionDAO();
RedisManager redisManager = redisManager();
sessionDAO.setRedisManager(redisManager);
return sessionDAO;
}