session存在的问题
spring-session
token取代session,实现服务端到客户端的跨变
没有什么是加一层解决不了的hhh
当我们用nginx做负载均衡时,用户在A服务器登录了,A服务器存储了session,客户端也存储了cookie,其中有JSESSIONID。
此时负载均衡,访问B服务器的话,B服务器是没有这个session的,客户端的cookie里边JSESSIONID也就找不到对应的session,相当于没有登录,此时如何解决呢?
用nginx的ip_hash可以使得某个ip的用户,只固定访问某个特定的服务器,这样就不会跑到其他服务器,也就不需要考虑session共享的问题了
但与此同时,这又违背了Nginx负载均衡的初衷,请求都固定打到某一台服务器,宕机就不好办了,于是我们有了spring-session
当请求进来的时候,SessionRepositoryFilter 会先拦截到请求,将 request 和 response 对象转换成 SessionRepositoryRequestWrapper 和SessionRepositoryResponseWrapper 。后续当第一次调用 request 的getSession方法时,会调用到 SessionRepositoryRequestWrapper 的getSession
方法。
这个方法是被重写过的,逻辑是先从 request 的属性中查找,如果找不到;再查找一个key值是"SESSION"的 Cookie,通过这个 Cookie 拿到 SessionId 去 Redis 中查找,如果查不到,就直接创建一个RedisSession 对象,同步到 Redis 中。
说的简单点就是:拦截请求,将之前在服务器内存中进行 Session 创建销毁的动作,改成在 Redis 中创建。
/**
* HttpServletRequest getSession()实现
*/
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//从当前请求获取sessionId
String requestedSessionId = getRequestedSessionId();
if (requestedSessionId != null
&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
S session = getSession(requestedSessionId);
if (session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
}
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException(
"For debugging purposes only (not an error)"));
}
//为当前请求创建session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
//更新时间
session.setLastAccessedTime(System.currentTimeMillis());
//对Spring session 进行包装(包装成HttpSession)
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
/**
* 根据sessionId获取session
*/
private S getSession(String sessionId) {
S session = SessionRepositoryFilter.this.sessionRepository
.getSession(sessionId);
if (session == null) {
return null;
}
session.setLastAccessedTime(System.currentTimeMillis());
return session;
}
/**
* 从当前请求获取sessionId
*/
@Override
public String getRequestedSessionId() {
return SessionRepositoryFilter.this.httpSessionStrategy
.getRequestedSessionId(this);
}
private void setCurrentSession(HttpSessionWrapper currentSession) {
if (currentSession == null) {
removeAttribute(CURRENT_SESSION_ATTR);
}
else {
setAttribute(CURRENT_SESSION_ATTR, currentSession);
}
}
/**
* 获取当前请求session
*/
@SuppressWarnings("unchecked")
private HttpSessionWrapper getCurrentSession() {
return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
}
复制代码
查询我们搞懂了,很简单,其实就是透明的包装,我们拿还是直接用session.getAttributes(),那相应的也带来了问题
redis
中每个session存储了三条信息。
spring:session:expirations 为set结构, 存储1620393360000 时间点过期的 spring:session:sessions:expires 键值
第二个用来存储Session的详细信息,这个key的过期时间为Session的最大过期时间 + 5分钟。如果默认的最大过期时间为30分钟,则这个key的过期时间为35分钟。
spring:session:sessions为hash结构,主要内容:包括Session的过期时间间隔、最近的访问时间、attributes
hgetall spring:session:sessions:1b8b2340-da25-4ca6-864c-4af28f033327
1) "creationTime"
2) "\\\\xac\\\\xed\\\\x00\\\\x05sr\\\\x00\\\\x0ejava.lang.Long;\\\\x8b\\\\xe4\\\\x90\\\\xcc\\\\x8f#\\\\xdf\\\\x02\\\\x00\\\\x01J\\\\x00\\\\x05valuexr\\\\x00\\\\x10java.lang.Number\\\\x86\\\\xac\\\\x95\\\\x1d\\\\x0b\\\\x94\\\\xe0\\\\x8b\\\\x02\\\\x00\\\\x00xp\\\\x00\\\\x00\\\\x01j\\\\x9b\\\\x83\\\\x9d\\\\xfd"
3) "maxInactiveInterval"
4) "\\\\xac\\\\xed\\\\x00\\\\x05sr\\\\x00\\\\x11java.lang.Integer\\\\x12\\\\xe2\\\\xa0\\\\xa4\\\\xf7\\\\x81\\\\x878\\\\x02\\\\x00\\\\x01I\\\\x00\\\\x05valuexr\\\\x00\\\\x10java.lang.Number\\\\x86\\\\xac\\\\x95\\\\x1d\\\\x0b\\\\x94\\\\xe0\\\\x8b\\\\x02\\\\x00\\\\x00xp\\\\x00\\\\x00\\\\a\\\\b"
5) "lastAccessedTime"
6) "\\\\xac\\\\xed\\\\x00\\\\x05sr\\\\x00\\\\x0ejava.lang.Long;\\\\x8b\\\\xe4\\\\x90\\\\xcc\\\\x8f#\\\\xdf\\\\x02\\\\x00\\\\x01J\\\\x00\\\\x05valuexr\\\\x00\\\\x10java.lang.Number\\\\x86\\\\xac\\\\x95\\\\x1d\\\\x0b\\\\x94\\\\xe0\\\\x8b\\\\x02\\\\x00\\\\x00xp\\\\x00\\\\x00\\\\x01j\\\\x9b\\\\x83\\\\x9d\\\\xfd"
复制代码
处理一个session为什么要存储三条数据,而不是一条呢!对于session的实现,需要监听它的创建、过期等事件,redis可以监听某个key的变化,当key发生变化时,可以快速做出相应的处理。
Redis中过期key的策略有两种:
也就是:无法保证key的过期时间抵达后立即生成过期事件【把session给销毁】。 这也侧面说明了,前端访问的时候,是先拿服务器的Tocamt本地缓存,而不是拿redis,也就导致了,redis的键一直没有被访问,即使expire到了,也还是没被及时访问,没法触发过期事件
redis 是一个存储键值数据库系统,那它源码中是如何存储所有键值对的呢?
Redis 本身是一个典型的 key-value 内存存储数据库,因此所有的 key、value 都保存在之前学习过的 Dict 结构中。不过在其 database 结构体中,有两个 Dict:一个用来记录 key-value;另一个用来记录 key-TTL。
内部结构
这里有两个问题需要我们思考:
惰性删除:顾明思议并不是在 TTL 到期后就立刻删除,而是在访问一个 key 的时候,检查该 key 的存活时间,如果已经过期才执行删除。
周期删除:通过一个定时任务,周期性的抽样部分过期的 key,然后执行删除。执行周期有两种:
SLOW 模式规则:
FAST 模式规则(过期 key 比例小于 10% 不执行 ):
spring-session为了能够及时的产生Session过期时的过期事件,所以增加了:
spring:session:sessions:expires:726de8fc-c045-481a-986d-f7c4c5851a67
spring:session:expirations:1620393360000
复制代码
spring-session中有个定时任务,每个整分钟都会查询相应的spring:session:expirations:【整分钟的时间戳 中的过期SessionId】
然后再访问一次这个SessionId,即spring:session:sessions:expires:SessionId ,【相当于主动访问这个key ,此时会触发redis的过期发生】——即本地缓存的Session过期事件。
可能有同学会问?这不跟redis的第二个过期策略一样吗,都是去扫一遍,有必要这里再扫吗?
定时任务代码
@Scheduled(cron = "0 * * * * *")
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
复制代码
定时任务每整分运行,执行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy实例,是RedisSession过期策略。
public void cleanExpiredSessions() {
// 获取当前时间戳
long now = System.currentTimeMillis();
// 时间滚动至整分,去掉秒和毫秒部分
long prevMin = roundDownMinute(now);
if (logger.isDebugEnabled()) {
logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
}
// 根据整分时间获取过期键集合,如:spring:session:expirations:1439245080000
String expirationKey = getExpirationKey(prevMin);
// 获取所有的所有的过期session
Set
将时间戳滚动至整分
static long roundDownMinute(long timeInMs) {
Calendar date = Calendar.getInstance();
date.setTimeInMillis(timeInMs);
// 清理时间错的秒位和毫秒位
date.clear(Calendar.SECOND);
date.clear(Calendar.MILLISECOND);
return date.getTimeInMillis();
}
复制代码
获取过期Session的集合
String getExpirationKey(long expires) {
return this.redisSession.getExpirationsKey(expires);
}
// 如:spring:session:expirations:1439245080000
String getExpirationsKey(long expiration) {
return this.keyPrefix + "expirations:" + expiration;
}
复制代码
调用Redis的Exists命令,访问过期Session键,触发Redis键空间消息
/**
* By trying to access the session we only trigger a deletion if it the TTL is
* expired. This is done to handle
* https://github.com/spring-projects/spring-session/issues/93
*
* @param key the key
*/
private void touch(String key) {
this.redis.hasKey(key);
}
复制代码
这个留到下篇,我们再来详讲嘞,简单说就是:
客户端跟服务端,是1对多的关系,客户端只需要存储一份tokne即可,无需考虑共享问题 而若是服务端存【也就是session】,就需要考虑共享问题