四、redis缓存+超卖问题解决

一、redis缓存

尝试对前端页面进行相应的优化,比较典型的是缓存,一些东西可以存在浏览器身上或者redis中,提高相应速度,降低后端压力。
1 页面缓存
这里以商品列表页面为例。
原来的商品列表页面是这样写的:

@RequestMapping("to_list")
public String toList(Model model,MiaoshaUser user){
    if(user == null)
        return "login";
    model.addAttribute("user",user);
    List<GoodsVo> goodsVoList = goodsService.getGoodsVoList();
    model.addAttribute("goodsList",goodsVoList);
    return "goods_list";
}

给他添加页面缓存:

 @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 = redisService.get(GoodsKey.getGoodsList, "", String.class);
    	if(!StringUtils.isEmpty(html)) {
    		return html;
    	}
    	List<GoodsVo> goodsList = goodsService.listGoodsVo();
    	model.addAttribute("goodsList", goodsList);
//    	 return "goods_list";
    	SpringWebContext ctx = new SpringWebContext(request,response,
    			request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
    	//取不到,手动渲染(不太懂过程),再保存到redis
    	html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
    	if(!StringUtils.isEmpty(html)) {
    		redisService.set(GoodsKey.getGoodsList, "", html);
    	}
    	return html;
    }

对于商品详情页面的缓存,原来是这样写的:

@RequestMapping("/to_detail/{goodsId}")
public String toDetail(@PathVariable("goodsId") long goodsId,Model model, MiaoshaUser user){
    if(user == null)
        return "login";
    model.addAttribute("user",user);

    GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
    model.addAttribute("goods",goodsVo);
    long startAt = goodsVo.getStartDate().getTime();
    long endAt = goodsVo.getEndDate().getTime();
    long now = System.currentTimeMillis();
    int miaoshaStatus = 0;//秒杀活动的状态,0-秒杀前;1-正在秒杀;2-秒杀结束
    int remainSeconds = 0;//秒杀活动还剩多少秒
    if(now < startAt){
        miaoshaStatus = Constants.MiaoshaStatus.BEFORE_START;
        remainSeconds = (int)(startAt-now)/1000;
    }else if (now > endAt){
        miaoshaStatus = Constants.MiaoshaStatus.AFTER_MIAOSHA;
        remainSeconds = -1;
    }else {
        miaoshaStatus = Constants.MiaoshaStatus.ON_MIAOSHA;
        remainSeconds = 0;
    }

    model.addAttribute("miaoshaStatus",miaoshaStatus);
    model.addAttribute("remainSeconds",remainSeconds);
    return "goods_detail";
    }

现在改为如下,以goodsid作为区别:

@RequestMapping(value = "/to_detail/{goodsId}",produces = "text/html")
@ResponseBody
public String toDetail(@PathVariable("goodsId") long goodsId,Model model, MiaoshaUser user, HttpServletRequest request,
                       HttpServletResponse response) throws IOException{
    if(user == null){
        response.sendRedirect("/login/to_login");
        return null;
    }
    model.addAttribute("user",user);

    //先尝试从缓存中取
    String html = redisService.get(GoodsKey.getGoodsDetail,""+goodsId,String.class);
    if(!StringUtils.isEmpty(html)){
        return html;
    }

    GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
    model.addAttribute("goods",goodsVo);
    long startAt = goodsVo.getStartDate().getTime();
    long endAt = goodsVo.getEndDate().getTime();
    long now = System.currentTimeMillis();
    int miaoshaStatus = 0;//秒杀活动的状态,0-秒杀前;1-正在秒杀;2-秒杀结束
    int remainSeconds = 0;//秒杀活动还剩多少秒
    if(now < startAt){
        miaoshaStatus = Constants.MiaoshaStatus.BEFORE_START;
        remainSeconds = (int)(startAt-now)/1000;
    }else if (now > endAt){
        miaoshaStatus = Constants.MiaoshaStatus.AFTER_MIAOSHA;
        remainSeconds = -1;
    }else {
        miaoshaStatus = Constants.MiaoshaStatus.ON_MIAOSHA;
        remainSeconds = 0;
    }

    model.addAttribute("miaoshaStatus",miaoshaStatus);
    model.addAttribute("remainSeconds",remainSeconds);

    SpringWebContext ctx = new SpringWebContext(request,response,request.getServletContext(),
            request.getLocale(), model.asMap(),applicationContext);
    html = thymeleafViewResolver.getTemplateEngine().process("goods_detail",ctx);
    if(!StringUtils.isEmpty(html)){
        redisService.set(GoodsKey.getGoodsDetail,""+goodsId,html);
    }
    return html;
}

}

2 对象缓存
就是对一个对象进行缓存,比如这里可以对MiaoshaUser这个对象进行缓存

public MiaoshaUser getById(long id){
    //先去缓存取
    MiaoshaUser user = redisService.get(MiaoshaUserKey.getById,""+id,MiaoshaUser.class);
    if(user != null){
        return user;
    }
    //缓存没有则去数据库取
    user = miaoshaUserDao.getById(id);
    if(user != null){
        redisService.set(MiaoshaUserKey.getById,""+user.getId(),user);
    }
    return user;
}

这个逻辑是十分清晰的,但是如果我是更新一个信息呢?比如更新登录的用户的密码。那么就要注意,先更新数据库,在更新好数据库之后,一定要注意处理相关的缓存。(还是先处理缓存再处理数据库呢?)

public boolean updatePassword(String token, long id, String formPass) {
		//取user
		MiaoshaUser user = getById(id);
		if(user == null) {
			throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
		}
		//更新数据库
		MiaoshaUser toBeUpdate = new MiaoshaUser();
		toBeUpdate.setId(id);
		toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt()));
		miaoshaUserDao.update(toBeUpdate);
		//处理缓存
		redisService.delete(MiaoshaUserKey.getById, ""+id);
		user.setPassword(toBeUpdate.getPassword());
		redisService.set(MiaoshaUserKey.token, token, user);
		return true;
	}

二、 解决卖超

    @RequestMapping(value="/do_miaosha", method=RequestMethod.POST)
    @ResponseBody
    public Result<OrderInfo> miaosha(Model model,MiaoshaUser user,
    		@RequestParam("goodsId")long goodsId) {
    	model.addAttribute("user", user);
    	if(user == null) {
    		return Result.error(CodeMsg.SESSION_ERROR);
    	}
    	//判断库存
    	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);//10个商品,req1 req2
    	int stock = goods.getStockCount();
    	if(stock <= 0) {
    		return Result.error(CodeMsg.MIAO_SHA_OVER);
    	}
    	//判断是否已经秒杀到了
    	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
    	if(order != null) {
    		return Result.error(CodeMsg.REPEATE_MIAOSHA);
    	}
    	//减库存 下订单 写入秒杀订单
    	OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
        return Result.success(orderInfo);
    }

通过数据库的方式操作(应该有更好的方法)
1.SQL语句限制

GoodsDao 减库存方法更改如下(防止多个用户同时秒杀到最后一件商品,出现负),本质是利用数据库锁。

@Update("update miaosha_goods set stock_count = stock_count - 1 
where goods_id = #{goodsId} and stock_count > 0")

public int reduceStock(MiaoshaGoods g);

2.建立唯一索引(防止一个用户发送多个请求,秒杀到多件同类商品)
在这里插入图片描述
给用户建立唯一索引,如果插入几个相同用户,则订单创建失败

	@Transactional
	public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {
		OrderInfo orderInfo = new OrderInfo();
		orderInfo.setCreateDate(new Date());
		orderInfo.setDeliveryAddrId(0L);
		orderInfo.setGoodsCount(1);
		orderInfo.setGoodsId(goods.getId());
		orderInfo.setGoodsName(goods.getGoodsName());
		orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
		orderInfo.setOrderChannel(1);
		orderInfo.setStatus(0);
		orderInfo.setUserId(user.getId());
		long orderId = orderDao.insert(orderInfo);
		MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
		miaoshaOrder.setGoodsId(goods.getId());
		miaoshaOrder.setOrderId(orderId);
		miaoshaOrder.setUserId(user.getId());
		orderDao.insertMiaoshaOrder(miaoshaOrder);
		
		//将order存放到redis中
		redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder);
		 
		return orderInfo;
	}
	public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
		//return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
		return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class);
	}

部分参考:http://fossi.oursnail.cn/2019/04/23/miaosha/5.页面级高并发秒杀优化(Redis缓存+静态化分离)/

猜你喜欢

转载自blog.csdn.net/NIUBILISI/article/details/89881287