【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
	}

那么怎么做呢?
步骤:

  1. 创建一个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;
	}	
}
  1. 新建一个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);
	}		
}
  1. 现在就可以直接在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
	}
  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值