在分布式系统架构中,用户请求可能被负载均衡器分发到不同的服务器节点。如果用户的第一次请求落在服务器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会话共享内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论