spring session + redis 实现分布式session

引言

通常情况下,生产环境的web应用为了解决单点故障,都会多台部署,这样就会存在一个问题,当我们登录后,会将登录信息存储到session中,如果依赖原生的web容器中的session,当负载均衡到不同服务器会出现不同登录状态的情况,如下图:

                                             spring session + redis 实现分布式session_第1张图片

 当用户首次访问服务时,假设被Nginx代理到Server A,用户填写账户信息进行登录,此时,会在Server A上创建一个session保存登录信息,当用户继续访问服务时,可能会被Nginx代理到Server B,但Server B上没有找到这个session,此时又会引导用户去登录,显然这种处理是不能满足需求的。通常情况下,我们不会直接使用web容器的session,会将登录信息存储到第三方存储中(如redis),这时架构会演变成下面这样:

                         spring session + redis 实现分布式session_第2张图片

 这种架构在实际开发中也是比较常用的,今天我们来介绍另外一种比较常用的实现——spring session + redis。

一. 环境搭建

1. 在spring boot web环境基础上,添加以下配置

pom.xml



    org.springframework.session
    spring-session-data-redis




    org.springframework.boot
    spring-boot-starter-data-redis



    org.apache.commons
    commons-pool2

application.yml

server:
  port: 8080
  servlet:
    session:
#       Session timeout. 默认单位秒,不起作用,用@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 300000000)配置
#      timeout: 86400 # 24 * 60 * 60
      cookie:
        name: sts # 配置cookie名称
        # cookie timeout,seconds
        max-age: 3600
        

spring:
  ######## redis configuration #######
  redis:
    database: 5
    host: 192.168.0.121
#    host: 127.0.0.1
    port: 6379
    timeout: 5000
    password:
    lettuce:
      pool:
        max-idle: 9
        min-idle: 1
        max-active: 9
        max-wait: 5000
        
   ######## spring session configuration 这里用@EnableRedisHttpSession注解配置#######
#  session:
    # Session store type
#    store-type: redis
#    redis:
      # Session flush model
#      flush-mode: ON_SAVE
      # Namespace for keys used to store sessions.
#      namespace: spring:session

Application.java

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 30, redisNamespace = "CAS_SSO_SESSION")
@SpringBootApplication
public class SessionApplication {

	public static void main(String[] args) {
		SpringApplication.run(SessionApplication.class, args);
	}
}

LocalHttpSessionListener.java

/**
 * session生命周期监听
 * 
 */
@Configuration
public class LocalHttpSessionListener implements HttpSessionListener {
	
	@Override
	public void sessionCreated(HttpSessionEvent se) {
		HttpSession session = se.getSession();
		System.out.println(String.format("session[id=%s]被创建", session.getId()));
	}
	
	@Override
	public void sessionDestroyed(HttpSessionEvent se) {
		HttpSession session = se.getSession();
		System.out.println(String.format("session[id=%s]已过期", session.getId()));
	}
	
}

 SessionController.java

@RestController
public class SessionController {
	
	@GetMapping("/sessionId")
	public String getSessionId(HttpServletRequest request, HttpSession session) throws ServletException {
		String uid = null;
		if (request.getSession().getAttribute("uid") != null) {
			uid = request.getSession().getAttribute("uid").toString();
		}else {
			uid = UUID.randomUUID().toString();
		}
		request.getSession().setAttribute("uid", uid);
		return request.getSession().getId();
	}
	
}

2. 测试

    a. 启动服务

    b. 访问http://localhost:8080/sessionId 看redis中是否有数据存储

    c. 重启服务,再次访问http://localhost:8080/sessionId看uid是否依然存在

二. 主要配置类加载

1. 集成入口

RedisHttpSessionConfiguration:由EnableRedisHttpSession注解引入,主要初始化了以下重要类:

a. SessionRepositoryFilter: 过滤器,将web 容器的HttpServletRequest和HttpServletResponse分别代理为                                       SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper

b. RedisIndexedSessionRepository: redis session key 操作类

c. RedisMessageListenerContainer: redis key生命周期监听器

SessionAutoConfiguration: 由spring.factories引入,主要初始化了以下重要类:

a. DefaultCookieSerializer:cookie相关操作,如向response写入cookie

2. 时序图如下:

spring session + redis 实现分布式session_第3张图片

 

3. 相关session key

spring session + redis 实现分布式session_第4张图片

 如上图所示,一个session会存储为3个key。图中有一个id为b6c9ccd7-5670-4cdf-b509-ece45f3ca943的session,3个key分别为:

a. CAS_SSO_SESSION:sessions:b6c9ccd7-5670-4cdf-b509-ece45f3ca943

该key类型为Hashes,过期时间为maxInactiveIntervalSeconds + 300s (5min), 存储了session主要数据,如下图,保存了session的创建时间,最大有效时间(EnableRedisHttpSession注解中maxInactiveIntervalInSeconds属性值),最新访问时间,业务属性值(request.getSession().setAttribute("uid", uid))。

spring session + redis 实现分布式session_第5张图片

b. CAS_SSO_SESSION:sessions:expires:b6c9ccd7-5670-4cdf-b509-ece45f3ca943

该key类型为Strings,过期时间为maxInactiveIntervalSeconds(过期时间是以这个key为准),该key的value为空,和下面c中的key配合使用,我们可以监听这个key过期事件,来实现自己的业务(比如单点登录中的认证中心,session过期后,要清除所有的客户端本地session,就是通过这个功能实现的)。

c. CAS_SSO_SESSION:expirations:1891606440000

该key类型为Sets,过期时间为maxInactiveIntervalSeconds + 300s (5min),保存值为 expires:[sessionId],1891606440000是由lastAccessedTimeInMillis和maxInactiveInSeconds的下一个整分钟换算而来,源码如下:

static long expiresInMillis(Session session) {
	int maxInactiveInSeconds = (int) session.getMaxInactiveInterval().getSeconds();
	long lastAccessedTimeInMillis = session.getLastAccessedTime().toEpochMilli();
	return lastAccessedTimeInMillis + TimeUnit.SECONDS.toMillis(maxInactiveInSeconds);
}

// 参数timeInMs由上面expiresInMillis方法获得
static long roundUpToNextMinute(long timeInMs) {

	Calendar date = Calendar.getInstance();
	date.setTimeInMillis(timeInMs);
	date.add(Calendar.MINUTE, 1);
	date.clear(Calendar.SECOND);
	date.clear(Calendar.MILLISECOND);
	return date.getTimeInMillis();
}

 该key主要为了弥补Redis键空间通知不能保证过期键的通知的及时性,由定时任务执行,触发key的过期事件,源码如下:

// 定时任务执行, 为了弥补Redis键空间通知不能保证过期键的通知的及时性。
void cleanExpiredSessions() {
	long now = System.currentTimeMillis();
	long prevMin = roundDownMinute(now);

	if (logger.isDebugEnabled()) {
		logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
	}

	String expirationKey = getExpirationKey(prevMin);
	Set sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
	this.redis.delete(expirationKey);
	for (Object session : sessionsToExpire) {
		String sessionKey = getSessionKey((String) session);
		touch(sessionKey);
	}
}

/**
 * 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);
} 
  

调度会定时获取CAS_SSO_SESSION:expirations:1891606440000中的值(expires:b6c9ccd7-5670-4cdf-b509-ece45f3ca943),并查询key是否存在,如果该key已经过期了(由于某些原因并没有及时清理),会触发该key的过期事件

源码地址

你可能感兴趣的:(系统架构,spring,redis,session)