- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) > 編程語(yǔ)言 >
- 詳解Java分布式系統中session一致性問(wèn)題
在單機系統中,用戶(hù)登陸之后,服務(wù)端會(huì )保存用戶(hù)的會(huì )話(huà)信息,只要用戶(hù)不退出重新登陸,在一段時(shí)間內用戶(hù)可以一直訪(fǎng)問(wèn)該網(wǎng)站,無(wú)需重復登陸。用戶(hù)的信息存在服務(wù)端的 session 中,session中可以存放服務(wù)端需要的一些用戶(hù)信息,例如用戶(hù)ID,所屬公司companyId,所屬部門(mén)deptId等等。
但是隨著(zhù)業(yè)務(wù)的發(fā)展,技術(shù)架構需要調整,原來(lái)的單機系統逐漸被更換,架構由單機擴展到分布式,甚至當下流行的微服務(wù)。雖然在用戶(hù)端看來(lái)系統仍然是一個(gè)整體,但在技術(shù)端來(lái)說(shuō)業(yè)務(wù)則被拆分成多個(gè)模塊,各個(gè)模塊之間相互獨立,甚至不在同一臺物理機器上,模塊之間通過(guò) RPC 進(jìn)行通信。
那么原來(lái)單機只需一份的 session, 如何滿(mǎn)足在多系統的運行下保證會(huì )話(huà)一致性呢?單獨保存在任何一個(gè)系統中都不合適,而且每個(gè)單獨模塊系統也可能是分布式形式的,是由集群組成。那么session的分配就更復雜了。
針對以上問(wèn)題,我們可能會(huì )從以下幾個(gè)方面想到解決的方法,每個(gè)服務(wù)端存儲一份,通過(guò)同步的方式保證一致性,但是這種方式有個(gè)很明顯的缺點(diǎn):session的同步需要數據傳輸,占內網(wǎng)帶寬,有時(shí)延,網(wǎng)絡(luò )不穩定的時(shí)候會(huì )造成部分系統同步延遲,那么就不能保證 session 一致性。而且所有服務(wù)端都包含所有session數據,數據量受內存限制,無(wú)法水平擴展。
那么我們是否可以單獨將 session 信息存儲在某一個(gè)獨立的介質(zhì)中,介質(zhì)可以是DB也可以是緩存。
考慮到如下業(yè)務(wù):登陸的時(shí)候我們經(jīng)常會(huì )給用戶(hù)一個(gè)過(guò)期時(shí)間(一般移動(dòng)端常設置為7天或者一個(gè)月甚至更久),到期后用戶(hù)需要輸入登陸信息重新登陸,即會(huì )話(huà)過(guò)期。這種到期的設置我們自然想到了Redis的 key expire功能,所以最終我們可以將Redis引入進(jìn)來(lái)實(shí)現我們的這種需求。系統如下圖所示:
我們只需在用戶(hù)首次登陸的時(shí)候將用戶(hù)信息放到 Token并緩存到 Redis 中,同時(shí)設置一個(gè)過(guò)期時(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); // 設置用戶(hù) 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; }
那么在系統中如何使用呢,我們可以定義一個(gè)攔截器 SessionInterceptor
,當訪(fǎng)問(wèn) web 接口的時(shí)候檢驗用戶(hù)的 token 信息,判斷用戶(hù)是否登陸,未登錄的情況下一些業(yè)務(wù)接口是無(wú)法訪(fǎng)問(wèn)的,以及在登陸的情況下拿到我們需要的用戶(hù)信息,如 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超時(shí)"); throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout"); } User user = userService.getById(userId.longValue()); if (ObjectUtils.isEmpty(user)){ Log.debug("驗證token", "token驗證失敗,{}", "用戶(hù)信息不存在"); 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驗證失敗,用戶(hù)信息異常 userName : {}, status : {},deleteFlag : {}", user.getUserName(),user.getStatus(), user.getDeleteFlag()); throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout"); } return proceedingJoinPoint.proceed(); } }
以上實(shí)現方式簡(jiǎn)單易用,而且Redis 在分布式系統中的使用率也很高,所以無(wú)需額外的技術(shù)引入??梢灾С炙綌U展,數據庫或緩存水平切分即可,服務(wù)端重啟或者擴容都不會(huì )有session丟失的情況發(fā)生。
以上就是詳解Java分布式系統中session一致性問(wèn)題的詳細內容,更多關(guān)于Java分布式系統的資料請關(guān)注腳本之家其它相關(guān)文章!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。
Copyright ? 2009-2022 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 特網(wǎng)科技 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 百度云 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站