本文目录结构:
一、问题复现
二、问题排查
三、问题解决



一、问题复现

现在的项目都由原先的单体架构向分布式架构演变,在这个过程中就会存在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
		......
	}

综上两种方式,我选择第二种方式,主要是因为第一种方式会存在重复更新问题,性能较第二种稍差一些。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐