▶【SecKill】U5 页面优化技术

▶【SecKill】U5 页面优化技术

一、页面缓存+URL缓存+对象缓存

1、页面缓存

① com.kirin.miaosha.controller / GoodsController.java:

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @Autowired
    GoodsService goodsService;
    
    @Autowired
    ThymeleafViewResolver thymeleafViewResolver;
    
    @Autowired
    ApplicationContext applicationContext;
    
    //1.用户登录后,跳转到商品列表页
    @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);
        html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
        if(!StringUtils.isEmpty(html)) {
            redisService.set(GoodsKey.getGoodsList, "", html);
        }
        return html;
    }
    
    //2.查看商品详情
    @RequestMapping("/to_detail/{goodsId}") //根据id获取商品
    public String detail(Model model,MiaoshaUser user,@PathVariable("goodsId")long goodsId) {
        model.addAttribute("user", user);
        
        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        model.addAttribute("goods", goods);
        
        long startAt = goods.getStartDate().getTime();
        long endAt = goods.getEndDate().getTime();
        long now = System.currentTimeMillis(); 
        
        int miaoshaStatus = 0;
        int remainSeconds = 0;
        if(now < startAt ) {
            miaoshaStatus = 0;
            remainSeconds = (int)((startAt - now )/1000);
        }else if(now > endAt){
            miaoshaStatus = 2;
            remainSeconds = -1;
        }else {
            miaoshaStatus = 1;
            remainSeconds = 0;
        }
        model.addAttribute("miaoshaStatus", miaoshaStatus);
        model.addAttribute("remainSeconds", remainSeconds);
        return "goods_detail";
    }
}

* @RequestMapping(value = “/to_list”,produces = "text/html"):produces标注了返回值的类型,必须与@ResponseBody搭配使用
手动渲染过程中,我们要注入ThymeleafViewResolver,这个是框架给我们准备好的Bean,利用它来渲染页面,其中第二个参数,需要注入IContext。

* 在Spring5版本中,SpringWebContext已经没有了,我们需要使用WebContext来代替。它剔除了之前对ApplicationContext 过多的依赖,现在thymeleaf渲染不再过多依赖spring容器。

* 我们对Redis缓存的时间设置了60秒的限制,超过60秒过期,这个时间不宜过长,在60秒内看到的网页一直一样是可以接受的。

 

② 新建 com.kirin.miaosha.redis / GoodsKey.java:

package com.kirin.miaosha.redis;

public class GoodsKey extends BasePrefix{

    private GoodsKey(int expireSeconds, String prefix) {
        super(expireSeconds, prefix);
    }
    public static GoodsKey getGoodsList = new GoodsKey(60, "gl"); //有效期60s
    public static GoodsKey getGoodsDetail = new GoodsKey(60, "gd");
}

 

2、URL缓存(带参数)=页面缓存

① com.kirin.miaosha.controller / GoodsController.java:

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @Autowired
    GoodsService goodsService;
    
    @Autowired
    ThymeleafViewResolver thymeleafViewResolver;
    
    @Autowired
    ApplicationContext applicationContext;

    //1.用户登录后,跳转到商品列表页(缓存)
    @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);
        SpringWebContext ctx = new SpringWebContext(request,response,
                request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
        html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
        if(!StringUtils.isEmpty(html)) {
            redisService.set(GoodsKey.getGoodsList, "", html);
        }
        return html;
    }
    
    //2.查看商品详情(缓存)
    @RequestMapping(value="/to_detail/{goodsId}",produces="text/html") //根据id获取商品
    @ResponseBody
    public String detail2(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user,
            @PathVariable("goodsId")long goodsId) {
        model.addAttribute("user", user);
        
        String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);
        if(!StringUtils.isEmpty(html)) {
            return html;
        }
        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        model.addAttribute("goods", goods);
        
        long startAt = goods.getStartDate().getTime();
        long endAt = goods.getEndDate().getTime();
        long now = System.currentTimeMillis();
        
        int miaoshaStatus = 0;
        int remainSeconds = 0;
        if(now < startAt ) {
            miaoshaStatus = 0;
            remainSeconds = (int)((startAt - now )/1000);
        }else if(now > endAt)
            miaoshaStatus = 2; 
            remainSeconds = -1;
        }else { //秒杀进行中
            miaoshaStatus = 1;
            remainSeconds = 0;
        }
        model.addAttribute("miaoshaStatus", miaoshaStatus);
        model.addAttribute("remainSeconds", remainSeconds);
//        return "goods_detail";
        
        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;
    }
}

 

② 新建 com.kirin.miaosha.redis / GoodsKey.java:

package com.kirin.miaosha.redis;

public class GoodsKey extends BasePrefix{

    private GoodsKey(int expireSeconds, String prefix) {
        super(expireSeconds, prefix);
    }
    public static GoodsKey getGoodsList = new GoodsKey(60, "gl"); //有效期60s
    public static GoodsKey getGoodsDetail = new GoodsKey(60, "gd");
}

 

 

3、对象缓存

(1)对象缓存:com.kirin.miaosha.service / MiaoshaUserService.java / getByToken()

① com.kirin.miaosha.service / MiaoshaUserService.java:

 

② 新建 com.kirin.miaosha.redis / MiaoshaUserKey.java:

package com.kirin.miaosha.redis;
//3.实现类
public class MiaoshaUserKey extends BasePrefix{
    public static final int TOKEN_EXPIRE = 3600*24*2;
    private 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");
}

 

(2)更新缓存:com.kirin.miaosha.service / MiaoshaUserService.java / updatePassword()

① com.kirin.miaosha.service / MiaoshaUserService.java:

package com.kirin.miaosha.service;

@Service
public class MiaoshaUserService {
    
    public static final String COOKI_NAME_TOKEN = "token";
    
    @Autowired
    MiaoshaUserDao miaoshaUserDao;
    
    @Autowired
    RedisService redisService;
    
    //(1.对象缓存)通过id获取对象token
    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, ""+id, user);
        }
        return user;
    }
    
    //(2.缓存更新)修改密码
    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;
    }
    
    public String login(HttpServletResponse response, LoginVo loginVo) {
        if(loginVo == null) {
            throw new GlobalException(CodeMsg.SERVER_ERROR);
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        MiaoshaUser user = getById(Long.parseLong(mobile));
        if(user == null) {
            throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
        }
        //验证密码
        String dbPass = user.getPassword();
        String saltDB = user.getSalt(); 
        String calcPass = MD5Util.formPassToDBPass(formPass, saltDB); 
        if(!calcPass.equals(dbPass)) {
            throw new GlobalException(CodeMsg.PASSWORD_ERROR);
        }
        //登录成功后,生成cookie
        String token = UUIDUtil.uuid();
        addCookie(response, token, user);
        return token;
    }

    private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
        redisService.set(MiaoshaUserKey.token, token, user);
        Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token); //name:value = COOKI_NAME_TOKEN:token
        cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
        cookie.setPath("/");
        response.addCookie(cookie);
    }

    public MiaoshaUser getByToken(HttpServletResponse response, String token) {
        //参数校验
        if(StringUtils.isEmpty(token)) {
            return null;
        }
        MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
        //延长有效期
        if(user != null) {
            addCookie(response, token, user);
        }
        return user;
    }
}

 

② 新建 com.kirin.miaosha.redis / RedisService.java:

添加删除方法(com.kirin.miaosha.service / MiaoshaUserService.java / updatePassword()修改密码:删除之前的redis)

package com.kirin.miaosha.redis;

@Service
public class RedisService {
    
    @Autowired
    JedisPool jedisPool;
    
    //获取当前对象
    public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            String str = jedis.get(realKey);
            T t = stringToBean(str, clazz); //将获取的值,从String类型转换成bean类型
            return t;
        }finally {
            returnToPool(jedis);
        }
    }
    
    //设置对象
    public <T> boolean set(KeyPrefix prefix, String key,  T value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String str = beanToString(value);
            if(str == null || str.length() <= 0) {
                return false;
            }
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            int seconds = prefix.expireSeconds(); //过期时间
            if(seconds <= 0) {
                jedis.set(realKey, str);
            }else {
                jedis.setex(realKey, seconds, str);
            }
            return true;
        }finally {
            returnToPool(jedis);
        }
    }
    
    //判断key是否存在
    public <T> boolean exists(KeyPrefix prefix, String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            return jedis.exists(realKey);
        }finally {
            returnToPool(jedis);
        }
    }
    
    //增加值:+1
    public <T> Long incr(KeyPrefix prefix, String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            return jedis.incr(realKey);
        }finally {
            returnToPool(jedis);
        }
    }
    
    //减少值:-1
    public <T> Long decr(KeyPrefix prefix, String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            return jedis.decr(realKey);
        }finally {
            returnToPool(jedis);
        }
    }
    
    //删除(com.kirin.miaosha.service / MiaoshaUserService.java / updatePassword()修改密码:删除之前的redis)
    public boolean delete(KeyPrefix prefix, String key) {
         Jedis jedis = null;
         try {
             jedis =  jedisPool.getResource();
            //生成真正的key
            String realKey  = prefix.getPrefix() + key;
            long ret =  jedis.del(key);
            return ret > 0;
         }finally {
              returnToPool(jedis);
         }
    }
    
    //将设置的值,从bean类型转换成String类型
    private <T> String beanToString(T value) {
        if(value == null) { //若空,直接返回
            return null;
        }
        Class<?> clazz = value.getClass();
        if(clazz == int.class || clazz == Integer.class) { //若是int类型,直接写入
            return ""+value;
        }else if(clazz == String.class) { //若是String类型,直接写入
            return (String)value;
        }else if(clazz == long.class || clazz == Long.class) { //若是long类型,直接写入
            return ""+value;
        }else {
            return JSON.toJSONString(value);
        }
    }
    
    //将获取的值,从String类型转换成bean类型
    @SuppressWarnings("unchecked")
    private <T> T stringToBean(String str, Class<T> clazz) {
        if(str == null || str.length() <= 0 || clazz == null) { //若空,直接返回
            return null;
        }
        if(clazz == int.class || clazz == Integer.class) { //若是int类型,强转为int类型
            return (T)Integer.valueOf(str);
        }else if(clazz == String.class) { //若是String类型,直接输出
            return (T)str;
        }else if(clazz == long.class || clazz == Long.class) { //若是long类型,强转为long类型
            return (T)Long.valueOf(str);
        }else {
            return JSON.toJavaObject(JSON.parseObject(str), clazz);
        }
    }
    
    private void returnToPool(Jedis jedis) {
         if(jedis != null) {
             jedis.close();
         }
    }
    
}

 

③ 新建 com.kirin.miaosha.dao / MiaoshaUserDao.java:

package com.kirin.miaosha.dao;

@Mapper
public interface MiaoshaUserDao {
    
    @Select("select * from miaosha_user where id = #{id}")
    public MiaoshaUser getById(@Param("id")long id);

    //修改密码:MiaoshaUserService.java / updatePassword()
    @Update("update miaosha_user set password = #{password} where id = #{id}")
    public void update(MiaoshaUser toBeUpdate);
}

 

4、压测goods_list

(1)打jar包,放进Linux服务器

(2)启动miaosha_3.jar包,将输出放进nohup中

nohup java -jar miaosha_3.jar &

 

(3)启动

tail -f nohup.out

 

(4)压测

goods_list.jmx
并发10000 = 1000个线程 * 10次循环

 

 

二、页面静态化,前后端分离

1、商品详情静态化

① com.kirin.miaosha.controller / GoodsController.java:

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @Autowired
    GoodsService goodsService;
    
    @Autowired
    ThymeleafViewResolver thymeleafViewResolver;
    
    @Autowired
    ApplicationContext applicationContext;
    
    //2.3 查看商品详情(页面静态化:页面存为HTML,动态数据通过接口从服务端获取)
    @RequestMapping(value="/detail/{goodsId}")
    @ResponseBody
    public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user,
            @PathVariable("goodsId")long goodsId) {
        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        
        long startAt = goods.getStartDate().getTime();
        long endAt = goods.getEndDate().getTime();
        long now = System.currentTimeMillis();
        int miaoshaStatus = 0;
        int remainSeconds = 0;
        if(now < startAt ) {
            miaoshaStatus = 0;
            remainSeconds = (int)((startAt - now )/1000);
        }else if(now > endAt){
            miaoshaStatus = 2;
            remainSeconds = -1;
        }else {
            miaoshaStatus = 1;
            remainSeconds = 0;
        }
        
        GoodsDetailVo vo = new GoodsDetailVo();
        vo.setGoods(goods);
        vo.setUser(user);
        vo.setRemainSeconds(remainSeconds);
        vo.setMiaoshaStatus(miaoshaStatus);
        return Result.success(vo);
    }
}

 

② 新建 com.kirin.miaosha.vo / GoodsDetailVo.java:

package com.kirin.miaosha.vo;

import com.kirin.miaosha.domain.MiaoshaUser;

public class GoodsDetailVo {
    private int miaoshaStatus = 0;
    private int remainSeconds = 0;
    private GoodsVo goods ;
    private MiaoshaUser user;
    public int getMiaoshaStatus() {
        return miaoshaStatus;
    }
    public void setMiaoshaStatus(int miaoshaStatus) {
        this.miaoshaStatus = miaoshaStatus;
    }
    public int getRemainSeconds() {
        return remainSeconds;
    }
    public void setRemainSeconds(int remainSeconds) {
        this.remainSeconds = remainSeconds;
    }
    public GoodsVo getGoods() {
        return goods;
    }
    public void setGoods(GoodsVo goods) {
        this.goods = goods;
    }
    public MiaoshaUser getUser() {
        return user;
    }
    public void setUser(MiaoshaUser user) {
        this.user = user;
    }
}

 

③ static / goods_detail.htm:

④ goods_list.html:

<a th:href="'/goods_detail.htm?goodsId='+${goods.id}">详情</a>

 

2、秒杀静态化

① static / goods_detail.htm:

<!DOCTYPE HTML>
<html>
<head>
    <title>商品详情</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!-- 1.商品详情静态化 -->
    <!-- jquery -->
    <script type="text/javascript" src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
    <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
    <!-- jquery-validator -->
    <script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
    <script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
    <!-- layer -->
    <script type="text/javascript" src="/layer/layer.js"></script>
    <!-- md5.js -->
    <script type="text/javascript" src="/js/md5.min.js"></script>
    <!-- common.js -->
    <script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
    <div class="panel panel-default">
        <div class="panel-heading">秒杀商品详情</div>
        <div class="panel-body">
            <span id="userTip"> 您还没有登录,请登陆后再操作<br/></span>
            <span>没有收货地址的提示。。。</span>
        </div>
        <table class="table" id="goodslist">
            <tr>  
                <td>商品名称</td>  
                <td colspan="3" id="goodsName"></td> 
            </tr>  
            <tr>  
                <td>商品图片</td>  
                <td colspan="3"><img id="goodsImg" width="200" height="200" /></td>  
            </tr>
            <tr>  
                <td>秒杀开始时间</td>  
                <td id="startTime"></td>
                <!-- 设置秒杀状态 -->
                <td>    
                    <input type="hidden" id="remainSeconds" />
                    <span id="miaoshaTip"></span>
                </td>
                <td>
                    <!--<form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">
                        <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
                        <input type="hidden" name="goodsId"  id="goodsId" />
                    </form>-->
                    <!-- 2.秒杀静态化 -->
                    <button class="btn btn-primary btn-block" type="button" id="buyButton"onclick="doMiaosha()">立即秒杀</button>
                    <input type="hidden" name="goodsId" id="goodsId" />
                </td>
            </tr>
            <tr>  
                <td>商品原价</td>  
                <td colspan="3" id="goodsPrice"></td>  
            </tr>
            <tr>  
                <td>秒杀价</td>  
                <td colspan="3" id="miaoshaPrice"></td>  
            </tr>
            <tr>  
                <td>库存数量</td>  
                <td colspan="3" id="stockCount"></td>  
            </tr>
        </table>
    </div>
</body>
<script>
    //2.秒杀静态化
    function doMiaosha(){
        $.ajax({
            url:"/miaosha/do_miaosha",
            type:"POST",
            data:{
                goodsId:$("#goodsId").val(), //获取goodsId
            },
            success:function(data){
                if(data.code == 0){
                    window.location.href="/order_detail.htm?orderId="+data.data.id;
                }else{
                    layer.msg(data.msg);
                }
            },
            error:function(){
                layer.msg("客户端请求有误");
            }
        });
    }
    
    //1.商品详情静态化:渲染页面
    function render(detail){
        var miaoshaStatus = detail.miaoshaStatus;
        var remainSeconds = detail.remainSeconds;
        var goods = detail.goods;
        var user = detail.user;
        if(user){ //若已登录,能获取到用户,就把提示信息隐藏
            $("#userTip").hide();
        }
        $("#goodsName").text(goods.goodsName);
        $("#goodsImg").attr("src", goods.goodsImg);
        $("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss"));
        $("#remainSeconds").val(remainSeconds);
        $("#goodsId").val(goods.id);
        $("#goodsPrice").text(goods.goodsPrice);
        $("#miaoshaPrice").text(goods.miaoshaPrice);
        $("#stockCount").text(goods.stockCount);
        countDown();
    }
    
    //页面初始化
    $(function(){
        //countDown();
        getDetail();
    });
    
    //1.商品详情静态化
    function getDetail(){
        var goodsId = g_getQueryString("goodsId"); //获取url参数(common.js)
        $.ajax({
            url:"/goods/detail/"+goodsId,
            type:"GET",
            success:function(data){
                if(data.code == 0){ //成功
                    render(data.data); //根据服务端的数据,把页面渲染出了
                }else{ //失败
                    layer.msg(data.msg); //把错误消息打印出来
                }
            },
            error:function(){
                layer.msg("客户端请求有误");
            }
        });
    }
    
    //秒杀倒计时
    function countDown(){
        var remainSeconds = $("#remainSeconds").val();
        var timeout;
        if(remainSeconds > 0){//秒杀还没开始,倒计时
            $("#buyButton").attr("disabled", true);
            $("#miaoshaTip").html("秒杀倒计时:"+remainSeconds+"");
            timeout = setTimeout(function(){
                $("#countDown").text(remainSeconds - 1);
                $("#remainSeconds").val(remainSeconds - 1);
                countDown();
            },1000);
        }else if(remainSeconds == 0){//秒杀进行中
            $("#buyButton").attr("disabled", false);
            if(timeout){
                clearTimeout(timeout);
            }
            $("#miaoshaTip").html("秒杀进行中");
        }else{//秒杀已经结束
            $("#buyButton").attr("disabled", true);
            $("#miaoshaTip").html("秒杀已经结束");
        }
    }
</script>
</html>

 

② com.kirin.miaosha.controller / MiaoshaController.java:

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/miaosha")
public class MiaoshaController {

    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @Autowired
    GoodsService goodsService;
    
    @Autowired
    OrderService orderService;
    
    @Autowired
    MiaoshaService miaoshaService;
    
    //秒杀表单提交
    @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);
        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);
    }
}

 

3、订单详情静态化

① static / order_detail.htm:

<!DOCTYPE HTML>
<html>
<head>
    <title>订单详情</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!-- jquery -->
    <script type="text/javascript" src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
    <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
    <!-- jquery-validator -->
    <script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
    <script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
    <!-- layer -->
    <script type="text/javascript" src="/layer/layer.js"></script>
    <!-- md5.js -->
    <script type="text/javascript" src="/js/md5.min.js"></script>
    <!-- common.js -->
    <script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
    <div class="panel panel-default">
        <div class="panel-heading">秒杀订单详情</div>
        <table class="table" id="goodslist">
            <tr>  
                <td>商品名称</td>  
                <td colspan="3" id="goodsName"></td> 
            </tr>  
            <tr>  
                <td>商品图片</td>  
                <td colspan="2"><img  id="goodsImg" width="200" height="200" /></td>  
            </tr>
            <tr>  
                <td>订单价格</td>  
                <td colspan="2"  id="orderPrice"></td>  
            </tr>
            <tr>
                 <td>下单时间</td>  
                <td id="createDate" colspan="2"></td>  
            </tr>
            <tr>
                 <td>订单状态</td>  
                <td id="orderStatus"></td>  
                <td>
                    <button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button>
                </td>
            </tr>
            <tr>
                 <td>收货人</td>  
                <td colspan="2">XXX  18812341234</td>  
            </tr>
            <tr>
                 <td>收货地址</td>  
                <td colspan="2">北京市昌平区回龙观龙博一区</td>  
            </tr>
        </table>
    </div>
</body>
<script>
    //渲染页面
    function render(detail){
        var goods = detail.goods;
        var order = detail.order;
        $("#goodsName").text(goods.goodsName);
        $("#goodsImg").attr("src", goods.goodsImg);
        $("#orderPrice").text(order.goodsPrice);
        $("#createDate").text(new Date(order.createDate).format("yyyy-MM-dd hh:mm:ss"));
        //订单状态
        var status = "";
        if(order.status == 0){
            status = "未支付"
        }else if(order.status == 1){
            status = "待发货";
        }
        $("#orderStatus").text(status);
        
    }
    
    $(function(){
        getOrderDetail();
    })
    
    //获取订单详情
    function getOrderDetail(){
        var orderId = g_getQueryString("orderId");
        $.ajax({
            url:"/order/detail",
            type:"GET",
            data:{
                orderId:orderId
            },
            success:function(data){
                if(data.code == 0){
                    render(data.data);
                }else{
                    layer.msg(data.msg);
                }
            },
            error:function(){
                layer.msg("客户端请求有误");
            }
        });
    }
</script>
</html>

 

② 新建 com.kirin.miaosha.controller / OrderController.java:

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/order")
public class OrderController {

    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @Autowired
    OrderService orderService;
    
    @Autowired
    GoodsService goodsService;
    
    @RequestMapping("/detail")
    @ResponseBody
    public Result<OrderDetailVo> info(Model model,MiaoshaUser user,
            @RequestParam("orderId") long orderId) {
        if(user == null) { //用户为空,提示重新登录
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        OrderInfo order = orderService.getOrderById(orderId);
        if(order == null) { //订单为空,提示错误
            return Result.error(CodeMsg.ORDER_NOT_EXIST);
        }
        long goodsId = order.getGoodsId(); //获取订单中的商品id
        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); //通过商品id获取商品
        OrderDetailVo vo = new OrderDetailVo();
        vo.setOrder(order);
        vo.setGoods(goods);
        return Result.success(vo);
    }
}

 

③ 新建 com.kirin.miaosha.vo / OrderDetailVo.java:

package com.kirin.miaosha.vo;

import com.kirin.miaosha.domain.OrderInfo;

public class OrderDetailVo {
    private GoodsVo goods;
    private OrderInfo order;
    public GoodsVo getGoods() {
        return goods;
    }
    public void setGoods(GoodsVo goods) {
        this.goods = goods;
    }
    public OrderInfo getOrder() {
        return order;
    }
    public void setOrder(OrderInfo order) {
        this.order = order;
    }
}

 

④ com.kirin.miaosha.service / OrderService.java:

package com.kirin.miaosha.service;

@Service
public class OrderService {
    
    @Autowired
    OrderDao orderDao;
    
    @Autowired
    RedisService redisService;
    
    public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
        return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
    }
    
    public OrderInfo getOrderById(long orderId) {
        return orderDao.getOrderById(orderId);
    }

    //生成订单
    @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);
        return orderInfo;
    }
}

 

⑤ com.kirin.miaosha.dao / OrderDao.java:

package com.kirin.miaosha.dao;

@Mapper
public interface OrderDao {
    
    @Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}")
    public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(@Param("userId")long userId, @Param("goodsId")long goodsId);

    @Insert("insert into order_info(user_id, goods_id, goods_name, goods_count, goods_price, order_channel, status, create_date)values("
            + "#{userId}, #{goodsId}, #{goodsName}, #{goodsCount}, #{goodsPrice}, #{orderChannel},#{status},#{createDate} )")
    @SelectKey(keyColumn="id", keyProperty="id", resultType=long.class, before=false, statement="select last_insert_id()")
    public long insert(OrderInfo orderInfo);
    
    @Insert("insert into miaosha_order (user_id, goods_id, order_id)values(#{userId}, #{goodsId}, #{orderId})")
    public int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder);    
    
    @Select("select * from order_info where id = #{orderId}")
    public OrderInfo getOrderById(@Param("orderId")long orderId);
}

 

⑥ com.kirin.miaosha.result / CodeMsg.java:

//订单模块5004XX
public static CodeMsg ORDER_NOT_EXIST = new CodeMsg(500400, "订单不存在");

 

4、★解决超卖问题

超卖问题:压测时,出现秒杀商品数量为 - 的情况

(1)修改SQL语句

com.kirin.miaosha.dao / GoodsDao.java:

 

(2)添加唯一索引

 

(3)压测,查看是否解决超卖问题

① 压测前,小优化:查缓存

com.kirin.miaosha.service / OrderService.java:

 

com.kirin.miaosha.redis / OrderKey.java:

package com.kirin.miaosha.redis;

public class OrderKey extends BasePrefix {

    public OrderKey(String prefix) {
        super(prefix);
    }
    public static OrderKey getMiaoshaOrderByUidGid = new OrderKey("moug");
}

 

② 压测

 

posted @ 2022-03-13 20:55  淇凌  阅读(149)  评论(0编辑  收藏  举报