一、缓存
1、session在项目中如何保存
教程在"用户模块"开发完成后,为了解决"单体应用重启后用户需重新登录"以及为"未来分布式部署"做准备,引入了Spring Session Data Redis来管理Session。
所以,Session的保存方式是:
-
Spring Session接管:当项目中加入了spring-session-data-redis依赖并进行了相应的Redis配置后,Spring Session会自动接管原生的javax.servlet.http.HttpSession的创建和管理。
-
存储到Redis:
-
当后端代码调用request.getSession().setAttribute("key", value)时,Spring Session会将这个Session属性(value)序列化后,存储到Redis中,而不是存储在Web服务器(如Tomcat)的内存里。
-
Session的ID仍然通过Cookie(通常是名为SESSION的Cookie)在客户端和服务器之间传递。
-
当后续请求到达时,Spring Session会根据请求中的Session ID Cookie,从Redis中查找对应的Session数据,并反序列化还原为HttpSession对象供应用代码使用。
-
**教程中的具体体现:**这里,safetyUser(脱敏后的User对象)就被Spring Session存入了Redis。
-
在UserServiceImpl.getLoginUser方法中,会执行:
javaObject userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
这里就是Spring Session从Redis中读取并反序列化用户对象。
三、用的是Redis的什么类型存储Session数据?
Spring Session Data Redis 在将Session数据存储到Redis时,默认情况下,主要使用以下几种Redis数据类型:
-
Hash (哈希):
-
主要存储Session属性:每个HTTP Session在Redis中通常会对应一个Hash类型的Key。
-
Key的格式:通常是 spring:session:sessions:{sessionId} (例如,spring:session:sessions:A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6)。
-
Hash的Fields和Values:
-
Hash的每个field对应Session的一个属性名(如sessionAttr:USER_LOGIN_STATE,其中USER_LOGIN_STATE是教程中定义的常量)。
-
Hash的每个value是该属性被序列化后的值(通常是使用Java序列化、JSON序列化或Kryo等)。
-
除了用户自定义的Session属性,Spring Session还会存储一些元数据属性,如creationTime, lastAccessedTime, maxInactiveInterval等,也作为Hash的field。
-
-
优点:使用Hash可以方便地对单个Session的多个属性进行原子性的增删改查,并且可以只获取或修改部分属性,而不需要读取和重写整个Session对象。
-
-
String (字符串) - 用于过期索引:
-
Spring Session会为每个Session在Redis中创建一个额外的String类型的Key,用于处理Session的过期。
-
Key的格式:通常是 spring:session:expirations:{roundedExpireTime},其中roundedExpireTime是一个向下取整的过期时间戳(例如,取整到分钟)。
-
Value:通常是一个Set或ZSet的结构(虽然Key类型是String,但其内容语义上可能表示一个集合),存储了所有在这个时间点会话将过期的sessionId。
-
作用:Spring Session会有一个后台任务(或者利用Redis自身的过期通知机制,如果配置了)来定期检查这些"过期索引"Key。当某个过期索引Key自身过期时(或者被任务扫描到时),就知道对应Value中存储的那些sessionId的会话已经过期了,然后就可以去清理这些Session的主数据(即上面提到的Hash Key)。
-
-
Set (集合) - 可能用于跟踪与特定主体(如用户)关联的Session:
-
在某些高级配置或特定场景下(例如,实现"查找用户所有活动会话"或"踢用户下线"功能),Spring Session可能会使用Set来存储与某个主体(如principalName,通常是用户名)关联的所有sessionId。
-
Key的格式:可能类似 spring:session:index:org.springframework.session.findByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:{username}。
-
Value:一个包含该用户所有活动sessionId的Set。
-
主要使用的是Hash类型来存储单个Session的属性和元数据。 其他类型(如String用于过期索引,Set用于主体索引)是辅助性的,用于实现Session的生命周期管理和特定查询功能。
教程中引入spring-session-data-redis后,这些底层的Redis数据结构使用对应用开发者是透明的,开发者仍然使用标准的HttpSession API进行操作。
2、Spring Session Data Redis 的实现原理
好的,面试官。Spring Session Data Redis 通过巧妙地集成和利用Redis,实现了将HTTP Session状态从Web服务器内存中剥离出来,存储到共享的Redis中,从而支持分布式会话、会话持久化和高可用性。
核心实现原理可以概括为以下几个关键点:
-
Servlet Filter 拦截与 HttpSession 代理:
-
SessionRepositoryFilter:Servlet过滤器。这个过滤器在HTTP请求处理链中处于较早的位置。
-
HttpServletRequest 包装:SessionRepositoryFilter会拦截进入的HttpServletRequest。它不会直接使用原生的HttpSession,而是用一个自定义的SessionRepositoryRequestWrapper来包装原始的request。
-
HttpSession 代理 :当应用代码、请求HttpSession时,这个包装器会返回一个由Spring Session管理的、代理的HttpSession实现(例如RedisIndexedSessionRepository.RedisSession或其包装类)。这个代理Session实现了标准的HttpSession接口。
-
-
SessionRepository 抽象与 Redis 实现:
-
SessionRepository接口:Spring Session定义了一个核心的SessionRepository<S extends Session>接口,它抽象了Session的创建、保存、检索、删除等操作。
-
RedisIndexedSessionRepository (或 RedisOperationsSessionRepository 在旧版本):这是SessionRepository接口的具体实现,它负责将Session数据与Redis进行交互。它内部会使用RedisOperations(通常是RedisTemplate)来执行实际的Redis命令。
-
-
Session 数据的 CRUD 操作与 Redis 交互:
-
创建Session (createSession()方法):
-
当应用第一次调用request.getSession(true)并且当前请求没有有效的Session ID时,Spring Session会创建一个新的Session实例(例如RedisSession)。
-
这个新的RedisSession会有一个新生成的唯一ID。
-
此时,通常还不会立即向Redis写入完整数据,可能只是在内存中创建了对象。
-
-
获取Session属性 (getAttribute(name)):
-
当调用代理HttpSession的getAttribute方法时,它会委托给RedisIndexedSessionRepository。
-
根据当前Session ID,从Redis中对应的Hash Key(如spring:session:sessions:{sessionId})里使用HGET命令获取指定属性名(如sessionAttr:USER_LOGIN_STATE)的值。
-
获取到的值(通常是序列化后的字节)会被反序列化后返回。
-
-
设置Session属性 (setAttribute(name, value)):
-
当调用代理HttpSession的setAttribute方法时,它会委托给RedisIndexedSessionRepository。
-
value会被序列化。
-
使用HSET命令将序列化后的value存储到Redis中对应Session ID的Hash Key下的指定属性名(如sessionAttr:USER_LOGIN_STATE)。
-
同时,Session的lastAccessedTime等元数据也会被更新。
-
-
移除Session属性 (removeAttribute(name)):
- 调用HDEL命令从Redis中对应Session ID的Hash Key里删除指定的属性。
-
Session失效 (invalidate()):
- 调用DEL命令删除Redis中与该Session ID相关的所有Key(包括主数据Hash Key、过期索引Key等)。
-
-
Session ID 管理与 Cookie:
-
生成Session ID:新的Session ID由Spring Session生成(通常是UUID)。
-
通过Cookie传递:当一个新的Session被创建,或者Session ID发生变化时,Spring Session会在HTTP响应中设置一个Cookie(默认名为SESSION),其值为新的Session ID。
-
从Cookie读取:对于后续请求,SessionRepositoryFilter会从请求的Cookie中读取SESSION的值,作为当前请求的Session ID,用于从RedisIndexedSessionRepository中加载Session数据。
-
-
Session 过期处理:
-
设置TTL :当Session数据写入Redis时(例如,通过HSET或HMSET更新属性),Spring Session会同时为该Session的主数据Hash Key(spring:session:sessions:{sessionId})设置一个TTL(Time To Live)。这个TTL通常是基于Session的maxInactiveInterval(最大不活动时间间隔)计算得出的。
-
Redis自动过期:当一个Session长时间没有被访问(即其lastAccessedTime没有更新,导致其TTL倒计时结束),Redis会自动删除这个Key。
-
过期索引(辅助机制):
-
如之前提到的,Spring Session还会维护一些"过期索引"Key(例如 spring:session:expirations:{roundedExpireTime},其Value是即将在此时间点过期的Session ID集合)。
-
Spring Session会有一个后台清理任务(或依赖Redis的Key过期事件通知机制,如果配置了Keyspace Notifications)来处理这些过期的Session。当一个过期索引Key本身过期时,就知道它关联的那些Session ID都过期了,可以进行批量的清理(虽然Redis的TTL机制通常是主要的过期方式)。
-
这个机制主要用于在某些情况下(例如,需要触发Session销毁事件HttpSessionListener.sessionDestroyed())能够更主动地感知到Session过期,而不仅仅依赖Redis的被动删除。
-
-
-
序列化:
- 存入Redis的Session属性值和部分元数据需要被序列化。Spring Session默认使用Java的内置序列化,但可以配置为使用其他序列化方式,如JSON(Jackson)、Kryo等,以提高性能或解决跨语言兼容性问题。
总结其实现原理的关键组件和流程:
-
Filter (SessionRepositoryFilter):拦截请求,包装Request/Response,负责Session的加载和提交。
-
HttpSession代理:应用代码操作的是一个代理Session,其操作被委托给SessionRepository。
-
SessionRepository (RedisIndexedSessionRepository):核心的CRUD接口实现,负责与Redis交互,使用RedisTemplate执行Redis命令。
-
Redis数据结构:主要使用Hash存储Session属性,使用String/Set等辅助过期和索引。
-
Cookie:用于在客户端和服务器之间传递Session ID。
-
TTL与过期机制:利用Redis的TTL实现Session的自动过期,并辅以过期索引进行更主动的清理。
通过这种方式,Spring Session Data Redis将HttpSession的生命周期管理和数据存储从单个Web服务器的内存中解放出来,使其成为一个可以被分布式应用实例共享的、更具弹性和可靠性的组件。应用代码基本无需修改,就能享受到分布式Session带来的好处。
3、spring session data redis 设计原理
好的,面试官。我将重新回答"Spring Session Data Redis是如何存储Session的,并重点阐述其设计原理和原因",力求更侧重于**"为什么这么设计"以及核心流程**。
一、核心目标:解决传统HttpSession的局限性
传统的Servlet HttpSession是存储在Web服务器(如Tomcat)内存中的,这在单体应用中工作良好,但在以下场景会遇到显著问题:
-
分布式/集群部署的挑战:当应用为了提升性能和可用性而部署到多个服务器实例时,每个实例都有自己独立的内存Session。如果用户的请求被负载均衡器分发到不同的实例,之前在一个实例上建立的Session信息在另一个实例上是不可见的,会导致用户需要频繁重新登录,体验极差。
-
应用重启/故障导致Session丢失:如果某个应用实例重启或发生故障,其内存中存储的所有Session数据都会丢失。这意味着所有在该实例上已登录的用户都需要重新认证,影响了服务的连续性。
-
水平扩展的困难:在不丢失Session数据的前提下,动态地增加或减少服务器实例变得困难,因为Session数据与特定实例绑定。
-
Session数据量对应用服务器内存的压力:如果Session中存储了大量数据,或者并发用户数非常多,会消耗宝贵的应用服务器JVM内存资源。
Spring Session Data Redis的核心目的,就是通过将HTTP Session数据外部化存储到共享的Redis中,来优雅地解决上述所有问题。
二、Spring Session Data Redis 的核心实现原理与设计原因:
-
为什么要通过Servlet Filter (SessionRepositoryFilter) 拦截所有HTTP请求?
-
设计原因/目的 :为了能够在应用代码处理请求之前就接管对HttpSession的控制权 ,并且这种接管对应用代码是透明的。应用代码希望继续使用标准的request.getSession() API,而不需要关心Session具体存在哪里。Filter是Servlet规范中实现这种早期介入和请求/响应修改的标准机制。
-
核心流程:当一个HTTP请求到达时,SessionRepositoryFilter会是请求处理链中非常靠前的一环。它会拦截这个请求,为后续的Session管理做准备。
-
-
为什么要对HttpServletRequest和HttpServletResponse进行包装?
-
设计原因/目的 :当应用代码调用request.getSession()时,我们不希望它返回由Web服务器(如Tomcat)管理的、存在于JVM内存中的原生HttpSession对象。我们希望它返回一个由Spring Session管理的、其数据实际来自于Redis的HttpSession对象。同样,当需要向客户端设置与Session相关的Cookie时(如Session ID),我们也需要通过包装后的response来确保使用的是Spring Session管理的ID。
-
核心流程:SessionRepositoryFilter会创建原始request和response的包装类(Wrapper,例如SessionRepositoryRequestWrapper)。这些包装类实现了与原始对象相同的接口(HttpServletRequest, HttpServletResponse),但重写了与Session相关的关键方法(如getSession())。后续的Servlet和应用代码操作的实际上是这些包装后的对象。
-
-
为什么getSession()返回的是一个代理的HttpSession实现?
-
设计原因/目的 :应用代码是面向标准的javax.servlet.http.HttpSession接口编程的。为了让应用代码无需任何修改就能使用基于Redis的Session,Spring Session必须提供一个完全实现了HttpSession接口的类。当应用代码对这个HttpSession对象进行操作(如setAttribute, getAttribute, invalidate)时,这些操作需要被拦截并转换为对Redis中Session数据的相应读写操作。代理模式是实现这种行为拦截和逻辑增强的经典方式。
-
核心流程:当应用代码通过包装后的request调用getSession()时,会返回一个Spring Session提供的代理HttpSession实例。这个代理对象的所有方法(如setAttribute, getAttribute)的内部实现都被Spring Session重写了,它们不再操作JVM内存,而是会调用SessionRepository去与Redis交互。
-
-
SessionRepository抽象层的作用是什么?为什么要与Redis交互?
-
设计原因/目的(SessionRepository抽象) :为了实现Session存储策略的可插拔性 和解耦。Spring Session的设计目标不仅仅是支持Redis,它也支持JDBC数据库、MongoDB、Hazelcast等多种后端存储。SessionRepository接口定义了Session数据访问的统一契约(创建、读取、更新、删除Session)。
-
设计原因/目的(与Redis交互):选择Redis作为Session存储后端,是因为Redis具有:
-
高性能:作为内存数据库,读写速度极快,适合高并发的Session访问。
-
数据共享:Redis是独立的服务,可以被所有分布式部署的应用实例共享访问。
-
丰富的数据结构:特别是Hash结构,非常适合存储Session的多个属性。
-
TTL(Time-To-Live)过期机制:可以方便地实现Session的自动过期。
-
-
核心流程:RedisIndexedSessionRepository(或类似实现)是SessionRepository针对Redis的具体实现。它接收来自代理HttpSession的操作请求,并将这些请求转换为实际的Redis命令(如使用RedisTemplate执行HSET, HGET, DEL等)来操作存储在Redis中的Session数据。
-
-
为什么通过Cookie传递Session ID?
-
设计原因/目的 :HTTP协议本身是无状态的。为了在连续的HTTP请求之间识别同一个用户的会话,需要在客户端和服务器之间传递一个唯一的会话标识符。Cookie是Web浏览器与服务器之间传递这种状态信息的最标准、最常用的机制。
-
核心流程:
-
当Spring Session(通过RedisIndexedSessionRepository)为一个新的用户会话创建一个Session时,会生成一个唯一的Session ID。
-
这个Session ID会通过包装后的response对象,以Set-Cookie响应头的形式(默认Cookie名为SESSION)发送给客户端浏览器。
-
浏览器在后续向同域发送的每个请求中,会自动在Cookie请求头中带上这个SESSION Cookie。
-
SessionRepositoryFilter在接收到请求时,会从Cookie请求头中解析出Session ID,然后用这个ID去RedisIndexedSessionRepository中查找对应的Session数据。
-
-
-
如何利用Redis的特性进行Session管理(如过期)?
-
设计原因/目的:Session不能无限期存在,必须有自动过期机制来释放资源,防止Redis内存被耗尽。Redis的Key过期(TTL)功能天然适合实现这一点,且效率很高。
-
核心流程:
-
当Spring Session向Redis存储或更新一个Session的数据时(例如,通过HSET更新Session属性),它会同时为该Session在Redis中的主键(例如spring:session:sessions:{sessionId})设置一个TTL(Time-To-Live)。
-
这个TTL是根据Session的maxInactiveInterval(最大不活动时间间隔,即多久不操作就过期)计算得出的。每次访问Session(导致lastAccessedTime更新)时,这个TTL会被刷新。
-
当一个Session长时间没有活动,其在Redis中的主键的TTL倒计时结束时,Redis会自动删除这个键及其关联的数据,从而实现了Session的自动过期清理。
-
Spring Session还会使用一些辅助的Redis结构(如之前提到的过期索引)来更主动地跟踪和处理过期事件,例如为了能触发HttpSessionListener.sessionDestroyed()这样的标准回调。
-
-
通过以上这些精心设计,Spring Session Data Redis在对应用代码保持透明(仍然使用标准HttpSession API)的前提下,将Session管理无缝地切换到了基于Redis的分布式、高可用模式,极大地提升了Web应用的伸缩性和健壮性。其核心在于通过Filter和Wrapper拦截并代理了原生的Session操作,通过SessionRepository抽象了存储逻辑,并充分利用了Redis的高性能和特性。
二、项目难点
1、团队空间权限校验时,调用的接口/api 可能是图片模块,也有可能是空间模块。
问题:前端传递到后端的数据中有 id 字段,这个 id 可能是图片id也有可能是空间id。无法简单的确定 id 代表的是什么?可能是用户对图片进行操作或者用户对空间进行操作。


解决方案:



2、协同编辑时,如何保证编辑不会冲突
concurrentHashMap
3、同步可能造成堵塞,如何提高操作消息处理的效率
