分布式session
对每一次登陆的user用户都生成一个对应的token值放到cookie中,并将token与对应的user放到redis中保存。在对用户进行不同操作时都会重新生成cookie值以此延长cookie的生命周期。
//生成cookie
String token = UUIDUtil.uuid();
addCookie(response, token, user);
public void addCookie(HttpServletResponse response, String token, MiaoshaUser user){
myRedisUtil.set(MiaoshaUserKey.token, token, user); //将user和token绑定并存入Redis中
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token); //根据token生成cookie
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie); //将cookie放入response客户端中
}
页面缓存技术
将商品列表等这些变化不大的页面以整个页面的形式全部放到redis缓存中。根据实际情况设置一个过期时间,过期后再重新设置缓存。
采用SpringWebContext与thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);对页面进行手动渲染放到redis中。
@RequestMapping(value = "/to_list", produces = "text/html")
@ResponseBody
public String list(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) {
model.addAttribute("user", user);
//取缓存
String html = myRedisUtil.get(GoodsKey.getGoodsList, "", String.class);
if (!StringUtils.isEmpty(html)){
return html;
}
//查询商品列表,包括商品和秒杀商品
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList); //放到Model中,供前端展示使用。
// return "goods_list";
//手动渲染
SpringWebContext ctx = new SpringWebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap(), applicationContext);
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if (!StringUtils.isEmpty(html)){
myRedisUtil.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
对象缓存
将热点数据进行缓存,更加细粒度的缓存。
比如分布式session,将用户信息中放到redis缓存中。
取缓存——取数据库/更新数据库——更新缓存
public MiaoshaUser getById(long id){
//取缓存
MiaoshaUser miaoshaUser = myRedisUtil.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class);
if (miaoshaUser != null){
return miaoshaUser;
}
//取数据库
miaoshaUser = miaoshaUserDao.getById(id);
if (miaoshaUser != null){
//更新缓存
myRedisUtil.set(MiaoshaUserKey.getById, ""+id, miaoshaUser);
}
return miaoshaUser;
}
接口优化
减少数据库的访问
- 系统初始化,将商品库存数量加载到Redis
- 收到请求,Redis预减库存,库存不足,直接返回
- 放到rabbitmq队列中
- 请求出队,生成订单,减少库存
系统初始化,将商品数量加载到redis中
采用afterPropertiesSet方法
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsList = goodsService.listGoodsVo();
if (goodsList == null){
return;
}
for (GoodsVo goods:goodsList){
myRedisUtil.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
localOverMap.put(goods.getId(), false);
}
}
优化步骤
- 采用hashmap存放库存是否小于0。如果小于0则为false,返回秒杀失败。
- 预减库存:long stock = myRedisUtil.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId); decr方法
- 判断是否已经参与秒杀,如果参与秒杀则返回,没有参与秒杀则入队。
@RequestMapping(value = "/do_miaosha", method = RequestMethod.POST)
@ResponseBody
public Result<Integer> miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
model.addAttribute("user", user);
if (user == null){
return Result.error(CodeMsg.SESSION_ERROR);
}
//内存标记
boolean over = localOverMap.get(goodsId);
if (over){
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//预减库存
long stock = myRedisUtil.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);
if (stock < 0){
localOverMap.put(goodsId, true);
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (miaoshaOrder != null){
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//入队
MiaoshaMessage miaoshaMessage = new MiaoshaMessage();
miaoshaMessage.setUser(user);
miaoshaMessage.setGoodsId(goodsId);
mqSender.sendMiaoshaMessage(miaoshaMessage);
return Result.success(0);//排队中
/*
//判断秒杀库存
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goodsVo.getStockCount();
if (stock <= 0){
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (miaoshaOrder != null){
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//进行秒杀步骤:减库存 创建普通订单 创建秒杀订单 注意这是个事务操作
OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo);
return Result.success(orderInfo);
*/
}
rabbitmq异步下单
通过direct直连形式监听消费消息。
删减库存,判断是否可以下单。
生成订单。
@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
public void receive(String message){
log.info("receive message:" + message);
MiaoshaMessage miaoshaMessage = MyRedisUtil.stringToBean(message, MiaoshaMessage.class);
MiaoshaUser user = miaoshaMessage.getUser();
long goodsId = miaoshaMessage.getGoodsId();
//判断库存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);//10个商品,req1 req2
int stock = goods.getStockCount();
if(stock <= 0) {
return;
}
//判断是否已经秒杀到
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (miaoshaOrder != null){
return;
}
//减库存 下订单 写入秒杀订单
miaoshaService.miaosha(user, goods);
}
安全优化
秒杀接口地址隐藏
- 接口改造
- 添加生成地址的接口
- 秒杀收到请求,先验证
//建立新的秒杀path
public String createMiaoshaPath(MiaoshaUser user, long goodsId) {
if (user == null || goodsId <= 0){
return null;
}
String str = MD5Util.md5(UUIDUtil.uuid() + "123456");
myRedisUtil.set(MiaoshaKey.getMiaoshaPath, "" + user.getId() + "_" + goodsId, str);
return str;
}
//验证path
boolean check = miaoshaService.checkPath(user, goodsId, path);
if (!check){
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
数学公式验证码
点击秒杀之前,先输入验证码,分散用户的请求。
接口限流防刷
将访问次数放到redis中