SpringBoot秒殺系統實戰11-分佈式Session

文章目錄

我們的秒殺服務,實際的應用可能不止部署在一個服務器上,而是分佈式的多臺服務器,這時候假如用戶登錄是在第一個服務器,第一個請求到了第一臺服務器,但是第二個請求到了第二個服務器,那麼用戶的session信息就丟失了。
解決:session同步,無論訪問那一臺服務器,session都可以取得到。

本系統:利用一臺緩存服務器集中管理session,即利用緩存統一管理session。

分佈式Session的幾種實現方式

  1. 基於數據庫的Session共享
  2. 基於NFS共享文件系統
  3. 基於memcached 的session,如何保證 memcached 本身的高可用性?
  4. 基於resin/tomcat web容器本身的session複製機制
  5. 基於TT/Redis 或 jbosscache 進行 session 共享。
  6. 基於cookie 進行session共享

Session Replication 方式管理 (即session複製)

    簡介:將一臺機器上的Session數據廣播複製到集羣中其餘機器上

    使用場景:機器較少,網絡流量較小

    優點:實現簡單、配置較少、當網絡中有機器Down掉時不影響用戶訪問

    缺點:廣播式複製到其餘機器有一定廷時,帶來一定網絡開銷

Session Sticky 方式管理

簡介:即粘性Session、當用戶訪問集羣中某臺機器後,強制指定後續所有請求均落到此機器上

使用場景:機器數適中、對穩定性要求不是非常苛刻

優點:實現簡單、配置方便、沒有額外網絡開銷

缺點:網絡中有機器Down掉時、用戶Session會丟失、容易造成單點故障

緩存集中式管理

   簡介:將Session存入分佈式緩存集羣中的某臺機器上,當用戶訪問不同節點時先從緩存中拿Session信息

   使用場景:集羣中機器數多、網絡環境複雜

   優點:可靠性好

   缺點:實現複雜、穩定性依賴於緩存的穩定性、Session信息放入緩存時要有合理的策略寫入

轉載自:http://blog.csdn.net/u014352080/article/details/51764311

本系統解決思路:
用戶登錄成功之後,給這個用戶生成一個sessionId(用token來標識這個用戶),寫到cookie中,傳遞給客戶端。然後客戶端在隨後的訪問中,都在cookie中上傳這個token,然後服務端拿到這個token之後,就根據這個token來取得對應的session信息。token利用uuid生成。

業務邏輯Controller代碼:

    @RequestMapping("/do_login")//作爲異步操作
    @ResponseBody
    public Result<Boolean> doLogin(HttpServletResponse response,@Valid LoginVo loginVo) {//0代表成功
        //參數檢驗成功之後,登錄
        CodeMsg cm=miaoshaUserService.login(response,loginVo);
        if(cm.getCode()==0) {
            return Result.success(true);
        }else {
            return Result.error(cm);
        }
    }

MiaoshaUserService裏面login和addCookie以及getByToken操作:

	@Service
public class MiaoshaUserService {
	public static final String COOKIE1_NAME_TOKEN="token";	
	@Autowired
	MiaoshaUserDao miaoshaUserDao;
	@Autowired
	RedisService redisService;
	
	public CodeMsg login(HttpServletResponse response,LoginVo loginVo) {
		if(loginVo==null) {
			return CodeMsg.SERVER_ERROR;
		}
		//經過了依次MD5的密碼
		String mobile=loginVo.getMobile();
		String formPass=loginVo.getPassword();
		//判斷手機號是否存在
		MiaoshaUser user=getById(Long.parseLong(mobile));
		//查詢不到該手機號的用戶
		if(user==null) {
			return CodeMsg.MOBILE_NOTEXIST;
		}
		//手機號存在的情況,驗證密碼,獲取數據庫裏面的密碼與salt去驗證
		//111111--->e5d22cfc746c7da8da84e0a996e0fffa
		String dbPass=user.getPwd();
		String dbSalt=user.getSalt();
		//驗證密碼,計算二次MD5出來的pass是否與數據庫一致
		String tmppass=MD5Util.formPassToDBPass(formPass, dbSalt);
		if(!tmppass.equals(dbPass)) {
			return CodeMsg.PASSWORD_ERROR;
		}
		//生成cookie
		String token = UUIDUtil.uuid();
		addCookie(user,token,response);
		return CodeMsg.SUCCESS;		
	}
	/**
	 * 添加或者叫做更新cookie
	 */
	public void addCookie(MiaoshaUser user,String token,HttpServletResponse response) {
		// 可以用老的token,不用每次都生成cookie,可以用之前的
		System.out.println("uuid:" + token);
		// 將token寫到cookie當中,然後傳遞給客戶端
		// 此token對應的是哪一個用戶,將我們的私人信息存放到一個第三方的緩存中
		// prefix:MiaoshaUserKey.token key:token value:用戶的信息 -->以後拿到了token就知道對應的用戶信息。
		// MiaoshaUserKey.token-->
		redisService.set(MiaoshaUserKey.token, token, user);
		Cookie cookie = new Cookie(COOKIE1_NAME_TOKEN, token);
		// 設置cookie的有效期,與session有效期一致
		cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
		// 設置網站的根目錄
		cookie.setPath("/");
		// 需要寫到response中
		response.addCookie(cookie);
	}
	/**
	 * 從緩存裏面取得值,取得value
	 */
	public MiaoshaUser getByToken(String token,HttpServletResponse response) {
		if(StringUtils.isEmpty(token)) {
			return null;
		}		
		MiaoshaUser user=redisService.get(MiaoshaUserKey.token, token,MiaoshaUser.class);
		// 再次請求時候,延長有效期 重新設置緩存裏面的值,使用之前cookie裏面的token
		if(user!=null) {
			addCookie(user,token,response);
		}
		return user;
	}
}

客戶端在隨後的訪問中,都在cookie中上傳這個token,然後服務端拿到這個token之後,就根據這個token來去緩存中取得對應的(用戶信息)session信息
用戶登錄成功後,使用UUID生成一個token

public class UUIDUtil {
	public static String uuid() {
		return UUID.randomUUID().toString().replace("-", "");//去掉原生的"-",因爲原生會帶有"-"
	}
}

addCookie方法:
將MiaoshaUserKey前綴+sessionId(sessionId即token)組成了一個完整的Key,例如:“MiaoshaUserKey:tke67ad5b4ebbd4aef8e8bb36dab70c4fc”,其中MiaoshaUserKey前綴=“MiaoshaUserKey:tk”,token=“e67ad5b4ebbd4aef8e8bb36dab70c4fc”,然後作爲Key,和對應的用戶信息user(user對象信息會轉換爲字符串類型)一起存入Redis 緩存中。此token對應的是哪一個用戶,將我們的私人信息存放到一個第三方的緩存中,當訪問其他頁面的時候,就可以從cookie中獲取 token,再訪問redis 拿到用戶信息來判斷登錄情況了。

/**
* 添加或者叫做更新cookie
 */
public void addCookie(MiaoshaUser user,String token,HttpServletResponse response) {
// 可以用老的token,不用每次都生成cookie,可以用之前的
	System.out.println("uuid:" + token);
	// 將token寫到cookie當中,然後傳遞給客戶端
	// 此token對應的是哪一個用戶,將我們的私人信息存放到一個第三方的緩存中
	// prefix:MiaoshaUserKey.token key:token value:用戶的信息 -->以後拿到了token就知道對應的用戶信息。
	//這裏的token,即是一個包裝好有效期的一個Key的前綴,詳情請看下面MiaoshaUserKey
	redisService.set(MiaoshaUserKey.token, token, user);		
	Cookie cookie = new Cookie(COOKIE1_NAME_TOKEN, token);
	// 設置cookie的有效期,與session有效期一致
	cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
	// 設置網站的根目錄
	cookie.setPath("/");
	// 需要寫到response中
	response.addCookie(cookie);
}

當登錄成功後跳轉到訪問商品詳細頁面的時候,使用客戶端傳來的cookie信息或者是參數信息裏面的COOKIE1_NAME_TOKEN值即token值取得,使用getByToken去緩存裏面取得user的信息。(其中COOKIE1_NAME_TOKEN=“token”)

	@RequestMapping("/to_list")
	public String toList(Model model,@CookieValue(value=MiaoshaUserService.COOKIE1_NAME_TOKEN)String cookieToken,
			@RequestParam(value=MiaoshaUserService.COOKIE1_NAME_TOKEN)String paramToken,HttpServletResponse response) {
		//通過取到cookie,首先取@RequestParam沒有再去取@CookieValue
		if(StringUtils.isEmpty(paramToken)&&StringUtils.isEmpty(cookieToken)) {
			return "login";//返回到登錄界面
		}
		String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;	
		MiaoshaUser user=miaoshaUserService.getByToken(token,response);
		model.addAttribute("user", user);
		return "goods_list";//返回頁面login
	}

這裏就是登錄成功之後,服務器已經給客戶端的cookie裏面設置了token=e67ad5b4ebbd4aef8e8bb36dab70c4fc,所以在後面請求商品頁面的時候,會帶上這個cookie信息(token信息),那麼就可以根據該token信息去緩存裏面取得相對應的用戶信息了,從而實現了分佈式session。使用註解@RequestParam和@CookieValue是取得客戶端請求中對應的token信息。

MiaoshaUserKey :作爲Key的前綴的包裝類,具有有效期expireSeconds和前綴字段prefix

public class MiaoshaUserKey extends BasePrefix{
	public static final int TOKEN_EXPIRE=3600*24*2;//3600S*24*2    =2天
	public MiaoshaUserKey(int expireSeconds,String prefix) {
		super(expireSeconds,prefix);
	}
	public static MiaoshaUserKey token=new MiaoshaUserKey(TOKEN_EXPIRE,"tk");
	//對象緩存一般沒有有效期,永久有效
	public static MiaoshaUserKey getById=new MiaoshaUserKey(0,"id");
}

優化

想辦法在直接在controller的請求的方法上面直接注入MiaoshaUser(用戶的信息),直接通過方法的參數就可以將獲取用戶的信息,從而簡化代碼。就像SpringMVC中的controller 方法中可以有很多參數可以直接使用(例如request和response對象),有些參數不需要傳值,就可以直接獲取到一樣

如下面的代碼:

@RequestMapping("/to_list")
public String toList(Model model,MiaoshaUser user) {
	model.addAttribute("user", user);
	//查詢商品列表
	List<GoodsVo> goodsList= goodsService.getGoodsVoList();
	model.addAttribute("goodsList", goodsList);
	return "goods_list";//返回頁面login
}

那麼怎麼做呢?
步驟:

  • 創建一個UserArgumentResolver類並且實現接口HandlerMethodArgumentResolver,然後重寫裏面的方法resolveArgument和supportsParameter方法,既然要讓MiaoshaUser的實例對象可以像SpringMVC中的controller 方法中的HttpServletRequest的實例對象request一樣可以直接使用,那麼解析前端傳來的cookie裏面的token或者請求參數裏面的token的業務邏輯就在這裏完成
	@Service//注意需要放到容器裏面,加上註解
	public class UserArgumentResolver implements HandlerMethodArgumentResolver{
	@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.COOKIE1_NAME_TOKEN);	
		//獲取cookie
		String cookieToken=getCookieValue(request,MiaoshaUserService.COOKIE1_NAME_TOKEN);		
		if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken))
		{
			return null;
		}
		String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;		
		MiaoshaUser user=miaoshaUserService.getByToken(token,response);			
		//去取得已經保存的user,因爲在用戶登錄的時候,user已經保存到threadLocal裏面了,因爲攔截器首先執行,然後纔是取得參數
		//MiaoshaUser user=UserContext.getUser();
		return user;
	}
	public String getCookieValue(HttpServletRequest request, String cookie1NameToken) {//COOKIE1_NAME_TOKEN-->"token"
		//遍歷request裏面所有的cookie
		Cookie[] cookies=request.getCookies();
		if(cookies!=null) {
			for(Cookie cookie :cookies) {
				if(cookie.getName().equals(cookie1NameToken)) {
					System.out.println("getCookieValue:"+cookie.getValue());
					return cookie.getValue();
				}
			}
		}
		System.out.println("No getCookieValue!");
		return null;
	}
	public boolean supportsParameter(MethodParameter parameter) {
		//返回參數的類型
		Class<?> clazz=parameter.getParameterType();
		return clazz==MiaoshaUser.class;
	}	
}
  • 新建一個WebConfig類繼承自WebMvcConfigurerAdapter,並且重寫方法addArgumentResolvers,並且注入之前寫好的UserArgumentResolver,因爲UserArgumentResolver 使用@Service標註,已經放到容器裏面了,所以這裏可以直接注入
	@Configuration
	public class WebConfig extends WebMvcConfigurerAdapter{
	@Autowired
	UserArgumentResolver userArgumentResolver;	
	/**
	 * 設置一個MiaoshaUser參數給,toList使用
	 */
	@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
		//將UserArgumentResolver註冊到config裏面去	
		argumentResolvers.add(userArgumentResolver);
	}		
}
  • 現在就可以直接在controller裏面的請求方法裏面獲取我們想要的MiaoshaUser參數了
@RequestMapping("/to_list")
public String toList(Model model,MiaoshaUser user) {
    model.addAttribute("user", user);
    //查詢商品列表
    List<GoodsVo> goodsList= goodsService.getGoodsVoList();
    model.addAttribute("goodsList", goodsList);
    return "goods_list";//返回頁面login
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章