当前位置: 代码网 > it编程>数据库>Redis > redis中session会话共享的三种方案

redis中session会话共享的三种方案

2025年08月05日 Redis 我要评论
在分布式系统架构中,用户请求可能被负载均衡器分发到不同的服务器节点。如果用户的第一次请求落在服务器a并创建了session,而第二次请求被路由到服务器b,服务器b无法识别该用户的session状态,导

在分布式系统架构中,用户请求可能被负载均衡器分发到不同的服务器节点。如果用户的第一次请求落在服务器a并创建了session,而第二次请求被路由到服务器b,服务器b无法识别该用户的session状态,导致用户需要重新登录,这显然是灾难性的用户体验。

三种解决方案

粘性会话(sticky sessions)

例如在nginx的负载均衡策略中,通过ip哈希等策略将同一个ip的用户请求固定到同一服务器中,这样session自然也没有失效。

缺点:单点故障风险高(服务器宕机导致session丢失);扩容时rehash引发路由混乱。

session复制

例如在tomcat集群中实现session复制,需通过修改配置文件使不同节点间自动同步会话数据。集群内所有服务器实时同步session数据。

缺点:同步开销随服务器数量指数级增长,引发网络风暴和内存浪费。

redis统一存储

springboot整合spring session,通过redis存储方式实现session共享。

通过集中存储session(如redis),实现:

  • 无状态扩展:新增服务器无需同步session,直接访问中央存储。
  • 高可用性:即使单服务器宕机,会话数据仍可从redis恢复,用户无感知。
  • 数据一致性:所有服务器读写同一份session数据,避免状态冲突

spring session + redis集成

添加依赖

在pom.xml中引入关键依赖:

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-data-redis</artifactid>
</dependency>

<dependency>
    <groupid>org.springframework.session</groupid>
    <artifactid>spring-session-data-redis</artifactid>
</dependency>

配置redis连接

在application.properties中加上redis的配置:

spring:
  data:
    redis:
      host: localhost
      port: 6379

redis配置类

需要注入一个名为springsessiondefaultredisserializer的序列化对象,用于在redis中写入对象时进行序列化,不然session中存入对象会抛出异常。

package com.morris.redis.demo.session;

import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.data.redis.serializer.genericjackson2jsonredisserializer;

@configuration
public class redisconfig {

    @bean
    public genericjackson2jsonredisserializer springsessiondefaultredisserializer() {
        // 需要注入一个名为springsessiondefaultredisserializer的序列化对象
        // 不然session中存入对象会抛出异常
        return new genericjackson2jsonredisserializer();
    }
}

不需要显示的通过注解@enableredishttpsession来开启session共享。

使用session

package com.morris.redis.demo.session;

import jakarta.servlet.http.httpsession;
import org.springframework.web.bind.annotation.*;

@restcontroller
public class authcontroller {

    @postmapping("/login")
    public string login(httpsession session, @requestbody user user) {
        // 验证用户凭证...
        session.setattribute("currentuser", user);
        return "登录成功,sessionid:" + session.getid();
    }

    @getmapping("/profile")
    @responsebody
    public user profile(httpsession session) {
        // 任意服务节点都能获取到相同session
        return (user) session.getattribute("currentuser");
    }
}

session共享验证

调用登录接口:

$ curl --location --request post 'http://172.23.208.1:8080/login' --header 'content-type: application/json' --data-raw '{"name": "morris"}' -v
note: unnecessary use of -x or --request, post is already inferred.
*   trying 172.23.208.1:8080...
* tcp_nodelay set
* connected to 172.23.208.1 (172.23.208.1) port 8080 (#0)
> post /login http/1.1
> host: 172.23.208.1:8080
> user-agent: curl/7.68.0
> accept: */*
> content-type: application/json
> content-length: 18
>
* upload completely sent off: 18 out of 18 bytes
* mark bundle as not supporting multiuse
< http/1.1 200
< set-cookie: session=zte0yjc5njitodfizs00zgywlwe0ndktytbjnmq4zjuxymyy; path=/; httponly; samesite=lax
< content-type: text/plain;charset=utf-8
< content-length: 63
< date: tue, 24 jun 2025 03:23:52 gmt
<
* connection #0 to host 172.23.208.1 left intact
登录成功,sessionid:e14b7962-81be-4df0-a449-a0c6d8f51bf2

可以看到返回的响应头中带有cookie,后续请求需要带上这个cookie去请求接口才能识别出用户。

查询用户信息:

$ curl --location --request get 'http://172.23.208.1:8080/profile' --cookie 'session=zte0yjc5njitodfizs00zgywlwe0ndktytbjnmq4zjuxymyy'
{"name":"morris"}

可以修改端口再启动一个服务,换个服务查询用户信息:

$ curl --location 'http://172.23.208.1:8082/profile' --cookie 'session=zte0yjc5njitodfizs00zgywlwe0ndktytbjnmq4zjuxymyy'
{"name":"morris"}

高级配置

自定义cookie配置(支持跨域)

@bean
public cookieserializer cookieserializer() {
    defaultcookieserializer serializer = new defaultcookieserializer();
    serializer.setcookiename("jsessionid");
    serializer.setdomainnamepattern("example.com");
    serializer.setcookiepath("/");
    return serializer;
}

spring session核心原理

sessionautoconfiguration

这就是为什么不需要使用注解@enableredishttpsession来开启session共享。

sessionautoconfiguration类中会引入redissessionconfiguration。

@configuration(proxybeanmethods = false)
@conditionalonmissingbean(sessionrepository.class)
@import({ redissessionconfiguration.class, jdbcsessionconfiguration.class, hazelcastsessionconfiguration.class,
    mongosessionconfiguration.class })
static class servletsessionrepositoryconfiguration {

}

redissessionconfiguration类中会引入redishttpsessionconfiguration:

@configuration(proxybeanmethods = false)
@conditionalonproperty(prefix = "spring.session.redis", name = "repository-type", havingvalue = "default", matchifmissing = true)
@import(redishttpsessionconfiguration.class)
static class defaultredissessionconfiguration {

而注解@enableredishttpsession引入的配置类也是redissessionconfiguration:

@retention(java.lang.annotation.retentionpolicy.runtime)
@target({ java.lang.annotation.elementtype.type })
@documented
@import(springhttpsessionconfiguration.class)
public @interface enablespringhttpsession {

}

sessionrepositoryfilter

自定义过滤器sessionrepositoryfilter拦截所有请求,透明地替换了servlet容器原生的httpsession实现。

将请求包装为sessionrepositoryrequestwrapper:

protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain)
    throws servletexception, ioexception {
  request.setattribute(session_repository_attr, this.sessionrepository);

  sessionrepositoryrequestwrapper wrappedrequest = new sessionrepositoryrequestwrapper(request, response);
  sessionrepositoryresponsewrapper wrappedresponse = new sessionrepositoryresponsewrapper(wrappedrequest,
      response);

  try {
    filterchain.dofilter(wrappedrequest, wrappedresponse);
  }
  finally {
    wrappedrequest.commitsession();
  }
}

httpservletrequestwrapper

httpservletrequestwrapper中重写getsession()方法实现session会话替换。

public httpsessionwrapper getsession(boolean create) {
	httpsessionwrapper currentsession = getcurrentsession();
	if (currentsession != null) {
		return currentsession;
	}
	s requestedsession = getrequestedsession();
	if (requestedsession != null) {
		if (getattribute(invalid_session_id_attr) == null) {
			requestedsession.setlastaccessedtime(instant.now());
			this.requestedsessionidvalid = true;
			currentsession = new httpsessionwrapper(requestedsession, getservletcontext());
			currentsession.marknotnew();
			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 (sessionrepositoryfilter.this.httpsessionidresolver instanceof cookiehttpsessionidresolver
			&& this.response.iscommitted()) {
		throw new illegalstateexception("cannot create a session after the response has been committed");
	}
	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)"));
	}
	s session = sessionrepositoryfilter.this.sessionrepository.createsession();
	session.setlastaccessedtime(instant.now());
	currentsession = new httpsessionwrapper(session, getservletcontext());
	setcurrentsession(currentsession);
	return currentsession;
}

redissessionrepository

redissessionrepository负责创建redissession。

public redissession createsession() {
	mapsession cached = new mapsession(this.sessionidgenerator);
	cached.setmaxinactiveinterval(this.defaultmaxinactiveinterval);
	redissession session = new redissession(cached, true);
	session.flushifrequired();
	return session;
}

redissession

session保存时使用的是sessionredisoperations,其实就是redistemplate,这个redistemplate是spring session自己创建的,而不是使用的项目中的。

private void save() {
			savechangesessionid();
			savedelta();
			if (this.isnew) {
				this.isnew = false;
			}
		}

private void savedelta() {
  if (this.delta.isempty()) {
    return;
  }
  string key = getsessionkey(getid());
  redissessionrepository.this.sessionredisoperations.opsforhash().putall(key, new hashmap<>(this.delta));
  redissessionrepository.this.sessionredisoperations.expireat(key,
      instant.ofepochmilli(getlastaccessedtime().toepochmilli())
        .plusseconds(getmaxinactiveinterval().getseconds()));
  this.delta.clear();
}

到此这篇关于redis中session会话共享的三种方案的文章就介绍到这了,更多相关redis session会话共享内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网! 

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com