1. 实现商品列表页、详情页

2. 秒杀功能

3. 压力测试

4. 项目优化——(缓存)

4.1.页面缓存

由于该项目前后端不分离,因此每次获取页面时,每次我们都需要进行查询渲染。这里我们考虑用redis做缓存,缓存页面。

首先缓存商品列表页
在GoodsController中,引入redis依赖。在跳转页面的RequestMapping中,添加produces参数。
页面缓存起来需要的操作:

从redis里读取缓存
1. 如果有页面,直接返回
2. 如果没有,手动渲染模板
3. 缓存到redis中,并把结果返回给输出端

注入依赖:

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private ThymeleafViewResolver thymeleafViewResolver;

添加ResponseBody注解,在RequestMapping中添加produce参数,返回整个页面:

/**
 * 跳转到商品列表页面
 **/
@RequestMapping(value = "/toList",produces = "text/html;charset=utf-8")
@ResponseBody
public String toList(Model model, User user, HttpServletRequest request, HttpServletResponse response){
    //redis中获取页面,如果不为空,直接返回页面
    ValueOperations valueOperations = redisTemplate.opsForValue();
    String html = (String)valueOperations.get("goodsList");
    if(StringUtils.hasLength(html)){
        return html;
    }
    model.addAttribute("user", user);
    model.addAttribute("goodsList", goodsService.findGoodsVo());
    //如果为空,手动渲染,存入redis,再返回
    WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
    html = thymeleafViewResolver.getTemplateEngine().process("goodsList", webContext);
    //这里每60s会失效
    if(StringUtils.hasLength(html)){
        valueOperations.set("goodsList", html, 60, TimeUnit.SECONDS);
    }
    return html;
}

4.2. 对象缓存

在登录的时,我们已经将用户的信息保存到redis中了。
同时用户大部分时候是不更新的,基本上不设置过期时间;但是用户一旦做了变更,如修改密码,那我们需要删除redis中对应的缓存。

public RespBean updatePassword(String userTicket, String password, HttpServletRequest request, HttpServletResponse response) {
        TUser user = getUserByCookie(userTicket, request, response);
        if (user == null) {
            throw new GlobalException(RespBeanEnum.MOBILE_NOT_EXIST);
        }
        //设置新的密码
        user.setPassword(MD5Util.inputPassToDBPass(password, user.getSalt()));
        int result = tUserMapper.updateById(user);
        if (1 == result) {
            //如果该用户存在,则需要删除Redis中的数据
            redisTemplate.delete("user:" + userTicket);
            return RespBean.success();
        }
        return RespBean.error(RespBeanEnum.PASSWORD_UPDATE_FAIL);
    }

4.3 页面静态化(减少需要传输的数据)

  • 虽然缓存页面提升了速度,但是还是存在一些问题。渲染整个html,占用存储大,且发送到前端的时候,发送时,发送量还是很大的。
    -因此我们将页面静态化,即前后端分离。变动的数据通过ajax发送就行,不变的部分静态化。
    前端通过ajax获取信息
$(function () {
        // countDown();
        getDetails();
    });

   function getDetails() {
        var goodsId = g_getQueryString("goodsId");
        console.log(goodsId);
        $.ajax({
            url: '/goods/detail/' + goodsId,
            type: 'GET',
            success: function (data) {
                if (data.code == 200) {
                    render(data.object);
                    countDown();
                } else {
                    layer.msg("客户端请求出错");
                }
            },
            error: function () {
                layer.msg("客户端请求出错");
            }
        })
    }

后端

@ApiOperation("商品详情")
    @GetMapping("/detail/{goodsId}")
    @ResponseBody
    public RespBean toDetail(TUser user, @PathVariable Long goodsId) {
        GoodsVo goodsVo = itGoodsService.findGoodsVobyGoodsId(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date nowDate = new Date();
        //秒杀状态
        int seckillStatus = 0;
        //秒杀倒计时
        int remainSeconds = 0;

        if (nowDate.before(startDate)) {
            //秒杀还未开始0
            remainSeconds = (int) ((startDate.getTime() - nowDate.getTime()) / 1000);
        } else if (nowDate.after(endDate)) {
            //秒杀已经结束
            seckillStatus = 2;
            remainSeconds = -1;
        } else {
            //秒杀进行中
            seckillStatus = 1;
            remainSeconds = 0;
        }
        //返回的实体类
        DetailVo detailVo = new DetailVo();
        detailVo.setTUser(user);
        detailVo.setGoodsVo(goodsVo);
        detailVo.setRemainSeconds(remainSeconds);
        detailVo.setSecKillStatus(seckillStatus);
        return RespBean.success(detailVo);
    }

5. 解决商品超卖的问题

  1. 在减库存时,判断库存是否足够(更新有行级锁,因此不会出现并发修改)
OrderServiceImpl.java
	//秒杀商品表减库存
	SeckillGoods seckillGoods = seckillGoodsService.getOne(new QueryWrapper<SeckillGoods>().eq("goods_id",goods.getId()));     //查询秒杀商品
	seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);    //商品数减1
    seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count", seckillGoods.getStockCount()).eq("id", seckillGoods.getId()).gt("stock_count", 0)); //当商品的库存大于等于0时,再更新
  1. 解决同一用户同时秒杀多件商品——可以通过数据库建立唯一索引避免
    这里解决的是同一个人发起了两次请求时,可能两个都还正在操作,均未抢购成功,致判断为未抢购,导致用户多次秒杀。
    这里是线程安全的,利用了innodb行锁+复合索引的方式
  2. 商品系列 redis缓存 redis缓存商品列表页_java

  3. 将秒杀订单信息存入Redis,方便判断是否重复抢购时进行查询
/**
* 秒杀
* @return
*/
@Override
@Transactional
public Order seckill(User user, GoodsVo goods) {
   //秒杀商品表减库存
   SeckillGoods seckillGoods = seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",goods.getId()));
   seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
   boolean seckillGoodsResult = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count", seckillGoods.getStockCount()).eq("id", seckillGoods.getId()).gt("stock_count", 0));
   // seckillGoodsService.updateById(seckillGoods);
   if (!(goodsResult&&seckillGoodsResult)){
      return null;
   }
   //生成订单
   Order order = new Order();
   order.setUserId(user.getId());
   order.setGoodsId(goods.getId());
   order.setDeliveryAddrId(0L);
   order.setGoodsName(goods.getGoodsName());
   order.setGoodsCount(1);
   order.setGoodsPrice(seckillGoods.getSeckillPrice());
   order.setOrderChannel(1);
   order.setStatus(0);
   order.setCreateDate(new Date());
   orderMapper.insert(order);
   //生成秒杀订单
   SeckillOrder seckillOrder = new SeckillOrder();
   seckillOrder.setOrderId(order.getId());
   seckillOrder.setUserId(user.getId());
   seckillOrder.setGoodsId(goods.getId());
   seckillOrderService.save(seckillOrder);
   redisTemplate.opsForValue().set("order:" + user.getId() + ":" + goods.getId(), JsonUtil.object2JsonStr(seckillOrder));
   return order; }

取用户对应的秒杀商品表,若没有则抢购,否则返回已抢购过

String seckillOrderJson = (String) 
redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
      if (!StringUtils.isEmpty(seckillOrderJson)) {
         return RespBean.error(RespBeanEnum.REPEATE_ERROR);
     }
      Order order = orderService.seckill(user, goods);
      if (null != order) {
         return RespBean.success(order);
     }