spring-session-data-redis session更新失效问题解决
本文目录结构:一、问题复现二、问题排查三、问题解决一、问题复现现在的项目都由原先的单体架构向分布式架构演变,在这个过程中就会存在session共享的问题。单体架构只有一个JVM,所以内存中的数据可以共享,session直接保存在内存中,重启服务会导致session丢失,用户登录失效。分布式架构下基本都存在多个微服务,或者微服务集群,且版本迭代速度快,这些服务有各自的JVM,因此无法实现内存共享,这
本文目录结构:
一、问题复现
二、问题排查
三、问题解决
一、问题复现
现在的项目都由原先的单体架构向分布式架构演变,在这个过程中就会存在session共享的问题。
单体架构只有一个JVM,所以内存中的数据可以共享,session直接保存在内存中,重启服务会导致session丢失,用户登录失效。
分布式架构下基本都存在多个微服务,或者微服务集群,且版本迭代速度快,这些服务有各自的JVM,因此无法实现内存共享,这时就需要借助第三方存储空间(redis
)来实现内存共享,并且重启服务不会丢失session,能够保持用户登录态。
spring-session-data-redis
将session存放在redis进而实现服务间session共享,对于开发人员是透明的,无需改变现有session的使用方式。
解决了session共享问题,又有一个问题出现在眼前,那就是允许用户多设备登录(一个用户可以在多台终端登录,因此一个用户可能同事存在多个存活的session),因此如果用户在一个终端修改了session中的信息,则需要同步到其他存活的session。
举个例子:用户在两台手机上登录了账号,然后用户基本信息(昵称、头像等)会被保存到session中,当用户在其中一台设备修改基本信息,则需要自动同步其他的sesion,不然不同设备会出现不一致的情况。
/**
* 用户登录态,在登录成功后会生成sessionMember并保存到session中
*/
public class SessionMember {
private static final String SESSION_MEMBER = "member";
private String nickname;
private String avatar;
......
}
/**
* 用户基本信息
*/
public class User {
private Long id;
private String nickname;
private String avatar;
......
}
/**
* sessionMember 如果不存在则标识用户未登录,无法修改基本信息
* user 用户提交的基本信息
*/
public String update(@SessionAttribute(SessionMember.SESSION_MEMBER) SessionMember sessionMember, User user){
//TODO 更新关系型数据库中的用户基本信息
......
//TODO 查询该用户所有存活的sessionMember并更新
......
}
执行完update方法后,会发现sessionMember更新失效。
二、问题排查
通过debug
发现,redis
中的session
确实更新了,但是紧接着又被覆盖了,下面我们来分析下为什么会被覆盖。
1.解析请求参数
由于SessionMember
属性附带@SessionAttribute
注解,所有spring
最终调用ServletRequestAttributes
下的getAttribute
方法来解析参数(如果不清楚spring HttpMessageConverter原理,可以参考【HttpMessageConverter逻辑梳理】)。该方法有个容器sessionAttributesToUpdate
用来存放从session中获取的键值对。
org.springframework.web.servlet.mvc.method.annotation.SessionAttributeMethodArgumentResolver
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) {
return request.getAttribute(name, RequestAttributes.SCOPE_SESSION);
}
org.springframework.web.context.request.ServletRequestAttributes
public Object getAttribute(String name, int scope) {
if (scope == SCOPE_REQUEST) {
if (!isRequestActive()) {
throw new IllegalStateException(
"Cannot ask for request attribute - request is not active anymore!");
}
return this.request.getAttribute(name);
}
else {
HttpSession session = getSession(false);
if (session != null) {
try {
Object value = session.getAttribute(name);
if (value != null) {
this.sessionAttributesToUpdate.put(name, value);
}
return value;
}
catch (IllegalStateException ex) {
// Session invalidated - shouldn't usually happen.
}
}
return null;
}
}
2.处理接口响应
在处理完update
方法体内的业务逻辑后返回response响应,在finally中会调用requestCompleted
方法,该方法又调用updateAccessedSessionAttributes
方法去遍历上一步容器sessionAttributesToUpdate
中内容,并更新他们。这也就解释了为什么会出现更新失效的问题。
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
......
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
public void requestCompleted() {
executeRequestDestructionCallbacks();
updateAccessedSessionAttributes();
this.requestActive = false;
}
protected void updateAccessedSessionAttributes() {
if (!this.sessionAttributesToUpdate.isEmpty()) {
// Update all affected session attributes.
HttpSession session = getSession(false);
if (session != null) {
try {
for (Map.Entry<String, Object> entry : this.sessionAttributesToUpdate.entrySet()) {
String name = entry.getKey();
Object newValue = entry.getValue();
Object oldValue = session.getAttribute(name);
if (oldValue == newValue && !isImmutableSessionAttribute(name, newValue)) {
// 更新session(内部逻辑会将newValue个更新到redis中)
session.setAttribute(name, newValue);
}
}
}
catch (IllegalStateException ex) {
// Session invalidated - shouldn't usually happen.
}
}
this.sessionAttributesToUpdate.clear();
}
}
三、问题解决
1.重置sessionMember
并保存
/**
* sessionMember 如果不存在则标识用户未登录,无法修改基本信息
* user 用户提交的基本信息
*/
public String update(@SessionAttribute(SessionMember.SESSION_MEMBER) SessionMember sessionMember, User user, HttpServletRequest request){
sessionMember.setNickname(user.getNickname());
sessionMember.setavatar(user.getAvatar());
request.getSession().setAttribute(SessionMember.SESSION_MEMBER, sessionMember);
//TODO 更新关系型数据库中的用户基本信息
......
//TODO 更新该用户所有存活的sessionMember
......
}
2.禁用@SessionAttribute
注解
通过解析源码,我们知道是由于request参数解析时被记录到容器sessionAttributesToUpdate
中,那我们就规避它,不适用注解的方式从session中取值即可。
/**
* sessionMember 如果不存在则标识用户未登录,无法修改基本信息
* user 用户提交的基本信息
*/
public String update(User user, HttpServletRequest request){
SessionMember sessionMember = (SessionMember) requset.getSession().getAttribute(SessionMember.SESSION_MEMBER);
if (sessionMember == null) {
//TODO 提示用户未登录,去登陆
return;
}
//TODO 更新关系型数据库中的用户基本信息
......
//TODO 更新该用户所有存活的sessionMember
......
}
综上两种方式,我选择第二种方式,主要是因为第一种方式会存在重复更新问题,性能较第二种稍差一些。
更多推荐
所有评论(0)