原理
模型如下
nginx
nginx基于七層模型走的事HTTP協議,可以實現基于Lua直接繞開tomcat訪問redis,也可以作為靜態資源服務器,輕松扛下上萬并發, 負載均衡到下游tomcat服務器,打散流量。
我們都知道一臺4核8G的tomcat,在優化和處理簡單業務的加持下,大不了就處理1000左右的并發。
經過nginx的負載均衡分流后,利用集群支撐起整個項目,同時nginx在部署了前端項目后,更是可以做到動靜分離,進一步降低tomcat服務的壓力,這些功能都得靠nginx起作用,所以nginx是整個項目中重要的一環。
數據存儲
在tomcat支撐起并發流量后,我們如果讓tomcat直接去訪問Mysql,根據經驗Mysql企業級服務器只要上點并發,一般是16或32 核心cpu,32 或64G內存,像企業級mysql加上固態硬盤能夠支撐的并發,大概就是4000起~7000左右。
所以我們在高并發場景下,會選擇使用mysql集群,同時為了進一步降低Mysql的壓力,同時增加訪問的性能,我們也會加入Redis,同時使用Redis集群使得Redis對外提供更好的服務。
登錄流程
首先用戶點發送驗證碼->驗證碼存入session
用戶點登錄或注冊->檢查+處理用戶信息->用戶信息存入session
用戶請求(攜帶cookie)->cookie中攜帶者JsessionId到后臺,后臺通過JsessionId從session中拿到用戶信息->檢查用戶是否存在來攔截
攔截功能
//1.獲取sessionHttpSession session = request.getSession();//2.獲取session中的用戶Object user = session.getAttribute("user");//3.判斷用戶是否存在if(user == null){//4.不存在,攔截,返回401狀態碼response.setStatus(401);return false;}//5.存在,保存用戶信息到ThreadlocalUserHolder.saveUser((User)user);//6.放行return true;//攔截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);// token刷新的攔截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
Tomcat原理
當用戶發起請求時,會訪問我們像tomcat注冊的端口,任何程序想要運行,都需要有一個線程對當前端口號進行監聽,tomcat也不例外。當監聽線程知道用戶想要和tomcat連接連接時,那會由監聽線程創建socket連接,socket都是成對出現的,用戶通過socket互相傳遞數據。
當tomcat端的socket接受到數據后,此時監聽線程會從tomcat的線程池中取出一個線程執行用戶請求。
我們的服務部署到tomcat后,線程會找到用戶想要訪問的工程,然后用這個線程轉發到工程中的controller,service,dao中,并且訪問對應的DB。
在用戶執行完請求后,再統一返回,再找到tomcat端的socket,再將數據寫回到用戶端的socket,完成請求和響應。
隱藏敏感信息
核心思路就是寫一個UserDto對象,這個UserDto對象就沒有敏感信息了,我們在返回前,將有用戶敏感信息的User對象轉化成沒有敏感信息的UserDto對象
public static void saveUser(UserDTO user){tl.set(user);}public static UserDTO getUser(){return tl.get();}public static void removeUser(){tl.remove();}//保存用戶信息到session中session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));//存在,保存用戶信息到ThreadlocalUserHolder.saveUser((UserDTO) user);
Redis代替session
由于在第二臺服務器上,沒有第一臺服務器存放的session
早期的方案是session拷貝,就是說雖然每個tomcat上都有不同的session,但是每當任意一臺服務器的session修改時,都會同步給其他的Tomcat服務器的session
這樣會造成每臺服務器中都有完整的一份session數據,服務器壓力過大,下面用redis優化
session是每個用戶都有自己的session,但是redis的key是共享的,因此可以解決session共享問題
在設計這個key的時候,需要滿足key具有唯一性且方便攜帶,選擇在后臺生成一個隨機串token
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校驗手機號String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回錯誤信息return Result.fail("手機號格式錯誤!");}// 3.從redis獲取驗證碼并校驗String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 不一致,報錯return Result.fail("驗證碼錯誤");}// 4.一致,根據手機號查詢用戶 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();// 5.判斷用戶是否存在if (user == null) {// 6.不存在,創建新用戶并保存user = createUserWithPhone(phone);}// 7.保存用戶信息到 redis中// 7.1.隨機生成token,作為登錄令牌String token = UUID.randomUUID().toString(true);// 7.2.將User對象轉為HashMap存儲UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3.存儲String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.設置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);
}
登錄狀態刷新
由于攔截器綁定了刷新登錄token令牌的存活時間,如果當前用戶訪問了一些不需要攔截的路徑,那么這個攔截器就不會生效,所以此時令牌刷新的動作實際上就不會執行。
當前方案如下:
解決方案:添加一個攔截器,在第一個攔截器中攔截所有的路徑,把第二個攔截器做的事情放入到第一個攔截器中,同時刷新令牌,因為第一個攔截器有了threadLocal的數據,所以此時第二個攔截器只需要判斷攔截器中的user對象是否存在即可。
// 第一個攔截器
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.獲取請求頭中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}// 2.基于TOKEN獲取redis中的用戶String key = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判斷用戶是否存在if (userMap.isEmpty()) {return true;}// 5.將查詢到的hash數據轉為UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用戶信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用戶UserHolder.removeUser();}
}// 第二個攔截器
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.判斷是否需要攔截(ThreadLocal中是否有用戶)if (UserHolder.getUser() == null) {// 沒有,需要攔截,設置狀態碼response.setStatus(401);// 攔截return false;}// 有用戶,則放行return true;}
}
總結
這節主要分析了項目的整體架構,redis部分處理了短信驗證碼和登錄狀態驗證(代替session)