秒杀商城系统 分布式Session (八)

在搭建完集群环境后,不得不考虑的一个问题就是用户访问产生的session如何处理。如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在A、B两台服务器,用户在第一次访问网站时,Nginx通过其负载均衡机制将用户请求转发到A服务器,这时A服务器就会给用户创建一个Session。当用户第二次发送请求时,Nginx将其负载均衡到B服务器,而这时候B服务器并不存在Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。

如何实现session同步呢?此系统采用第三种。

在这里插入图片描述

解决思路:

用户登录成功之后,给这个用户生成一个sessionId(用token来标识这个用户),写到cookie中,传递给客户端。然后客户端在随后的访问中,都在cookie中上传这个token,然后服务端拿到这个token之后,就根据这个token来取得对应的session信息。
token利用uuid生成。

Controller层代码

		@RequestMapping("/do_login") //作为异步操作@ResponseBodypublic 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";	@AutowiredMiaoshaUserDao miaoshaUserDao;@AutowiredRedisService 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--->e5d22cfc746c7da8da84e0a996e0fffaString dbPass=user.getPwd();String dbSalt=user.getSalt();//验证密码,计算二次MD5出来的pass是否与数据库一致String tmppass=MD5Util.formPassToDBPass(formPass, dbSalt);if(!tmppass.equals(dbPass)) {return CodeMsg.PASSWORD_ERROR;}//生成cookieString 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里面的tokenif(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的前缀,详情请看下面MiaoshaUserKeyredisService.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没有再去取@CookieValueif(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);	//获取cookieString 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里面所有的cookieCookie[] 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标注,已经放到容器里面了,所以这里可以直接注入

	@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter{@AutowiredUserArgumentResolver userArgumentResolver;	/*** 设置一个MiaoshaUser参数给,toList使用*/@Overridepublic 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
}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部