詳解Java分布式系統(tǒng)中session一致性問題
業(yè)務(wù)場景
在單機系統(tǒng)中,用戶登陸之后,服務(wù)端會保存用戶的會話信息,只要用戶不退出重新登陸,在一段時間內(nèi)用戶可以一直訪問該網(wǎng)站,無需重復(fù)登陸。用戶的信息存在服務(wù)端的 session 中,session中可以存放服務(wù)端需要的一些用戶信息,例如用戶ID,所屬公司companyId,所屬部門deptId等等。

但是隨著業(yè)務(wù)的發(fā)展,技術(shù)架構(gòu)需要調(diào)整,原來的單機系統(tǒng)逐漸被更換,架構(gòu)由單機擴展到分布式,甚至當(dāng)下流行的微服務(wù)。雖然在用戶端看來系統(tǒng)仍然是一個整體,但在技術(shù)端來說業(yè)務(wù)則被拆分成多個模塊,各個模塊之間相互獨立,甚至不在同一臺物理機器上,模塊之間通過 RPC 進行通信。

那么原來單機只需一份的 session, 如何滿足在多系統(tǒng)的運行下保證會話一致性呢?單獨保存在任何一個系統(tǒng)中都不合適,而且每個單獨模塊系統(tǒng)也可能是分布式形式的,是由集群組成。那么session的分配就更復(fù)雜了。
Redis 實現(xiàn)
針對以上問題,我們可能會從以下幾個方面想到解決的方法,每個服務(wù)端存儲一份,通過同步的方式保證一致性,但是這種方式有個很明顯的缺點:session的同步需要數(shù)據(jù)傳輸,占內(nèi)網(wǎng)帶寬,有時延,網(wǎng)絡(luò)不穩(wěn)定的時候會造成部分系統(tǒng)同步延遲,那么就不能保證 session 一致性。而且所有服務(wù)端都包含所有session數(shù)據(jù),數(shù)據(jù)量受內(nèi)存限制,無法水平擴展。
那么我們是否可以單獨將 session 信息存儲在某一個獨立的介質(zhì)中,介質(zhì)可以是DB也可以是緩存。
考慮到如下業(yè)務(wù):登陸的時候我們經(jīng)常會給用戶一個過期時間(一般移動端常設(shè)置為7天或者一個月甚至更久),到期后用戶需要輸入登陸信息重新登陸,即會話過期。這種到期的設(shè)置我們自然想到了Redis的 key expire功能,所以最終我們可以將Redis引入進來實現(xiàn)我們的這種需求。系統(tǒng)如下圖所示:

我們只需在用戶首次登陸的時候?qū)⒂脩粜畔⒎诺?Token并緩存到 Redis 中,同時設(shè)置一個過期時間,偽代碼如下:
@Override
public Map login(UserDto dto) {
Map<String, Object> restMap = new HashMap<>();
// 校驗登陸信息
User user = checkLoginInfo(dto);
//刪除舊的token
String token = (String) redisUtils.get(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName());
if (!ObjectUtils.isEmpty(token)) {
redisUtils.delete(CacheConstants.USER_TOKEN_KEY_WEB + token);
}
// 唯一簽名信息
String signStr = user.getCompanyId() + user.getUserName() + dto.getPassword() + DateUtils.now().getTime();
token = MD5Utils.md5(signStr);
// 設(shè)置用戶 token
redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_WEB + token, user.getId(), LOGIN_EXPIRED_TIME);
//緩存新的token
redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName(), token, LOGIN_EXPIRED_TIME);
dto.setCompanyId(user.getCompanyId());
dto.setId(user.getId());
restMap.put("token", token);
restMap.put("userName", user.getUserName());
return restMap;
}
那么在系統(tǒng)中如何使用呢,我們可以定義一個攔截器 SessionInterceptor,當(dāng)訪問 web 接口的時候檢驗用戶的 token 信息,判斷用戶是否登陸,未登錄的情況下一些業(yè)務(wù)接口是無法訪問的,以及在登陸的情況下拿到我們需要的用戶信息,如 userId。
public class SessionInterceptor {
@Autowired
private RedisUtils redisUtils;
@Autowired
private UserService userService;
@Pointcut("execution(* com.jajian.demo.web.*.controller.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void controllerMethodPointcut() {
}
@Around("controllerMethodPointcut()")
public Object Interceptor(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
if (targetMethod.getDeclaringClass().isAnnotationPresent(NoLogin.class) || targetMethod.isAnnotationPresent(NoLogin.class)) {
return proceedingJoinPoint.proceed();
}
// 從獲取RequestAttributes中獲取HttpServletRequest的信息
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
Log.debug("驗證token", "token驗證失敗,{}", "token不存在");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
Integer userId= (Integer)redisUtils.get(CacheConstants.USER_TOKEN_KEY_WEB + token);
if (null == userId) {
Log.debug("驗證token", "token驗證失敗,{}", "token超時");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
User user = userService.getById(userId.longValue());
if (ObjectUtils.isEmpty(user)){
Log.debug("驗證token", "token驗證失敗,{}", "用戶信息不存在");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
if (user.getStatus() == UserStatusEnum.NO.getCode() || user.getDeleteFlag() == DeleteFlagEnum.YES.getCode()){
Log.debug("驗證token", "token驗證失敗,用戶信息異常 userName : {}, status : {},deleteFlag : {}", user.getUserName(),user.getStatus(), user.getDeleteFlag());
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
return proceedingJoinPoint.proceed();
}
}
以上實現(xiàn)方式簡單易用,而且Redis 在分布式系統(tǒng)中的使用率也很高,所以無需額外的技術(shù)引入??梢灾С炙綌U展,數(shù)據(jù)庫或緩存水平切分即可,服務(wù)端重啟或者擴容都不會有session丟失的情況發(fā)生。
以上就是詳解Java分布式系統(tǒng)中session一致性問題的詳細內(nèi)容,更多關(guān)于Java分布式系統(tǒng)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot如何使用自定義注解實現(xiàn)接口限流
這篇文章主要介紹了SpringBoot如何使用自定義注解實現(xiàn)接口限流,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06
java多線程事務(wù)加鎖引發(fā)bug用戶重復(fù)注冊解決分析
這篇文章主要為大家介紹了java多線程事務(wù)加鎖引發(fā)bug用戶重復(fù)注冊解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11
ExecutorService Callable Future多線程返回結(jié)果原理解析
這篇文章主要為大家介紹了ExecutorService Callable Future多線程返回結(jié)果,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09
mybatis的xml中使用@符號調(diào)用類方法示例
這篇文章主要為大家介紹了mybatis的xml中使用@符號調(diào)用類方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12
如何使用Idea搭建全注解式開發(fā)的SpringMVC項目
這篇文章主要介紹了如何使用Idea搭建全注解式開發(fā)的SpringMVC項目,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03

