通常秒殺項目可能不止部署在一個服務器上,而是使用分佈式部署在多臺服務器,這時候假如用戶登錄是在第一個服務器,第一個請求到了第一臺服務器,這是沒問題的;但是第二個請求到了第二個服務器,那麼用戶的Session信息就丟失了
解決:使用session同步,無論訪問那一臺服務器,session都可以取得到
本項目:利用一臺緩存服務器集中管理session,即利用緩存統一管理session
分佈式Session的幾種實現方式
- 使用Session Replication方式管理 (即session複製)
簡介:將一臺機器上的Session數據廣播複製到集羣中其餘機器上
使用場景:機器較少,網絡流量較小
優點:實現簡單、配置較少、當網絡中有機器Down掉時不影響用戶訪問
缺點:廣播式複製到其餘機器有一定廷時,帶來一定網絡開銷
- 使用Session Sticky方式管理
簡介:即粘性Session、當用戶訪問集羣中某臺機器後,強制指定後續所有請求均落到此機器上
使用場景:機器數適中、對穩定性要求不是非常苛刻
優點:實現簡單、配置方便、沒有額外網絡開銷
缺點:網絡中有機器Down掉時、用戶Session會丟失、容易造成單點故障
- 使用緩存集中式管理
簡介:將Session存入分佈式緩存集羣中的某臺機器上,當用戶訪問不同節點時先從緩存中拿Session信息
使用場景:集羣中機器數多、網絡環境複雜
優點:可靠性好
缺點:實現複雜、穩定性依賴於緩存的穩定性、Session信息放入緩存時要有合理的策略寫入
本項目分佈式Session的實現
實現思路:用戶登錄成功之後,給這個用戶生成一個sessionId(用token來標識這個用戶),並寫到cookie中傳遞給客戶端;然後客戶端在隨後的訪問中,都在cookie中傳遞這個token,服務端拿到這個token之後,就根據這個token來取得對應的session信息(token利用uuid生成)
登錄成功後給用戶生成sessionId:
生成sessionId的具體代碼:
addCookie方法解讀:將MiaoshaUserKey前綴+sessionId(sessionId即token)組成了一個完整的Key,例如:“MiaoshaUserKey:tk4470ee9b98eb4e63bbc52a4e9b65052e”,其中MiaoshaUserKey前綴=“MiaoshaUserKey:tk”,token=“4470ee9b98eb4e63bbc52a4e9b65052e”,作爲Key和對應的用戶信息(user對象信息會轉換爲字符串類型)一起存入Redis 緩存中;此token對應的是一個用戶,將用戶信息存放到一個第三方的緩存中,當訪問其他頁面的時候,就可以從cookie中獲取到token,再訪問redis拿到用戶信息來判斷登錄情況,存入redis中的內容如下:
客戶端在隨後的訪問中,都會在cookie中傳遞這個token,服務端拿到這個token之後,就根據這個token去緩存中取得對應的(用戶信息)session信息,如下:
後端驗證session代碼如下:
這裏就是登錄成功之後,後端把token以響應頭的形式返回給前端,然後在後面請求的時候,會帶上這個token,那麼後端就可以根據該token去緩存裏面取得相對應的用戶信息,從而實現分佈式session
像上面那樣使用@RequestParam和@CookieValue來獲取token比較麻煩,可想辦法直接在controller的請求方法上面直接注入MiaoshaUser(用戶的信息),然後直接通過方法的參數就可以獲取用戶的信息,從而簡化代碼;就像SpringMVC中的controller 方法中可以有很多參數可以直接使用(例如request和response對象),有些參數不需要傳值,就可以直接獲取到一樣
例如優化後的代碼:
優化校驗token令牌所需具體代碼
創建一個UserArgumentResolver類並且實現HandlerMethodArgumentResolver接口,然後重寫裏面的resolveArgument和supportsParameter方法,既然要讓MiaoshaUser這個實例對象可以像SpringMVC中的controller那樣直接使用HttpServletRequest的實例對象request,那麼解析前端傳來的token或者請求參數裏面的token的業務邏輯就在這裏完成:
package com.javaxl.miaosha_05.config;
import com.javaxl.miaosha_05.domain.MiaoshaUser;
import com.javaxl.miaosha_05.service.MiaoshaUserService;
import com.javaxl.miaosha_05.util.UserContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//將UserArgumentResolver註冊到config裏面去
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
//既然能注入service,那麼可以用來容器來管理,將其放在容器中
@Autowired
MiaoshaUserService miaoshaUserService;
public Object resolveArgument(MethodParameter arg0, ModelAndViewContainer arg1, NativeWebRequest webRequest,
WebDataBinderFactory arg3) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
//獲取cookie
String cookieToken = UserContext.getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
MiaoshaUser user = miaoshaUserService.getByToken(response, token);
return user;
}
public boolean supportsParameter(MethodParameter parameter) {
//返回參數的類型
Class<?> clazz = parameter.getParameterType();
return clazz == MiaoshaUser.class;
}
}
新建一個WebConfig類繼承WebMvcConfigurerAdapter,並且重寫addArgumentResolvers方法,並且注入之前寫好的UserArgumentResolver類,因爲UserArgumentResolver類使用了@Service進行標註,已經放到容器裏面了,所以這裏可以直接注入:
package com.javaxl.miaosha_05.config;
import com.javaxl.miaosha_05.interceptor.AccessInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
UserArgumentResolver userArgumentResolver;
@Autowired
AccessInterceptor accessInterceptor;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
//將UserArgumentResolver註冊到config裏面去
argumentResolvers.add(userArgumentResolver);
}
/**
* 註冊攔截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//註冊
registry.addInterceptor(accessInterceptor);
super.addInterceptors(registry);
}
}
然後就可以直接在controller裏面的方法裏獲取我們想要的MiaoshaUser參數並判斷session,如下:
從上述代碼來看,即使是優化過後,每個接口裏面也要寫一段重複的代碼:
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
所以,還需要進一步優化,使得這一段代碼不用重複寫,那就是使用自定義註解+Aop來處理token令牌的校驗
使用自定義註解+Aop來進一步優化token令牌校驗
在我們的系統裏,有的接口需要進行token令牌校驗,而有的接口是不需要進行token令牌校驗的,比如登錄接口;我們可以使用自定義註解來判斷接口是否需要進行token令牌校驗,使得系統更加靈活
新建一個自定義註解DisableToken,用於在切面裏判斷是否需要進行校驗,如果有則跳過token校驗,如果沒有則不跳過:
package com.javaxl.miaosha_05.annotation;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 作用在方法上,以及類上
*/
@Target({ METHOD, TYPE })
@Retention(RUNTIME)
@Inherited
public @interface DisableToken {
}
新建一個用於校驗token令牌的切面MiaoshaUserTokenAspect,判斷是否需要進行校驗也在這裏完成:
package com.javaxl.miaosha_05.aspect;
import com.javaxl.miaosha_05.annotation.DisableToken;
import com.javaxl.miaosha_05.domain.MiaoshaUser;
import com.javaxl.miaosha_05.exception.GlobalException;
import com.javaxl.miaosha_05.result.CodeMsg;
import com.javaxl.miaosha_05.service.MiaoshaUserService;
import com.javaxl.miaosha_05.util.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
@Aspect
@Component
@Slf4j
public class MiaoshaUserTokenAspect {
@Autowired
MiaoshaUserService userService;
@Pointcut("execution( * com.javaxl..controller.*.*(..))")
public void miaoshaUserTokenCut() {
}
@Around("miaoshaUserTokenCut()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Signature s = pjp.getSignature();
MethodSignature ms = (MethodSignature) s;
Method m = ms.getMethod();
Annotation[] annotations = m.getAnnotations();
for (Annotation annotation : annotations) {
//如果在方法上添加了DisableToken註解,那麼此方法是不需要token令牌就能訪問的
if (annotation instanceof DisableToken) {
//直接放行
return pjp.proceed(args);
}
}
int count = 0;
HttpServletRequest request = null;
HttpServletResponse response = null;
MiaoshaUser miaoshaUser = null;
//主要是對參數數組的中的秒殺User做封裝處理
int loop = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof HttpServletRequest) {
count++;
request = (HttpServletRequest) args[i];
} else if (args[i] instanceof HttpServletResponse) {
count++;
response = (HttpServletResponse) args[i];
} else if (args[i] instanceof MiaoshaUser) {
count++;
miaoshaUser = (MiaoshaUser) args[i];
loop = i;
}
}
if (count == 3) {
String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
String cookieToken = UserContext.getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
//如果前端沒傳token過來
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
throw new GlobalException(CodeMsg.SESSION_ERROR);
}
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
//根據token從緩存取用戶信息
miaoshaUser = userService.getByToken(response, token);
args[loop] = miaoshaUser;
}
Object ob = pjp.proceed(args);//ob爲方法的返回值
return ob;
}
}
使用如下:
校驗token令牌除了使用Aop,也可以使用攔截器,詳情可參考: