Java秒杀方案(2)

5.5 分布式Session

5.5.1 分布式Session问题

之前的代码在一台服务器上运行服务没有问题,当部署多太服务器系统,配合Nginx的时候会出现用户登录的问题。每台服务器都会存储一个Session,不同服务器的Session是不共用的。
原因:由于Nginx使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一发到后端的应用上。
刚开始我们在Tomcat1登录之后,用户信息放在Tomcat1的Session里。过一会,请求又被Nginx分发到Tomcat2上,Tomcat2上Session里还没有用户信息,于是又要登录。

image.png

解决方案

  • Session复制
    • 优点
      • 无需修改代码,只需要修改Tomcat配置
    • 缺点
      • Session同步传输占用内网带宽
      • 多台Tomcat同步性能指数级下降
      • Session占用内存,无法有效水平扩展
  • 前端存储
    • 优点
      • 不占用服务端内存
    • 缺点
      • 存在安全风险
      • 数据大小受cookie限制
      • 占用外网带宽
  • Session粘滞
    • 优点
      • 无需修改代码
      • 服务端可以水平扩展
    • 缺点
      • 增加新机器,会重新Hash,导致重新登录
      • 应用重启,需要重新登录
  • 后端集中存储
    • 优点
      • 安全
      • 容易水平扩展
    • 缺点
      • 增加复杂度
      • 需要修改代码

5.5.2 Redis实现分布式Session

redis的安装,基础操作,在这里就不提。

方法一:使用SpringSession实现

  • 添加依赖
<!--        spring data redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<!--        commons-pool2 对象池依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
<!--        spring-session-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>-->
  • application.yml
spring:
  # redis配置
  redis:
    #服务器地址
    host: 192.168.65.100
    #端口
    port: 6379
    # 数据库
    database: 0
    #超时时间
    timeout: 10000ms
    lettuce:
      pool:
        #最大连接数,默认8
        max-active: 8
        #最大李连杰阻塞等待时间,默认-1
        max-wait: 10000ms
        #最大空闲连接,默认8
        max-idle: 200
        # 最小空闲连接,默认0
        min-idle: 5

  • 测试:其余代码暂时不动,重新登录测试。会发现session已经存储在Redis上。

方法二:将用户信息存入Redis

  • 依赖
<!--        spring data redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<!--        commons-pool2 对象池依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
  • 添加配置
spring:
  # redis配置
  redis:
    #服务器地址
    host: 192.168.65.100
    #端口
    port: 6379
    # 数据库
    database: 0
    #超时时间
    timeout: 10000ms
    lettuce:
      pool:
        #最大连接数,默认8
        max-active: 8
        #最大李连杰阻塞等待时间,默认-1
        max-wait: 10000ms
        #最大空闲连接,默认8
        max-idle: 200
        # 最小空闲连接,默认0
        min-idle: 5
  • RedisConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //key序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //value序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //HashKey序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //HashValue序列化
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        //注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

5.5.3 优化登录功能

每个接口中都要判断用户是否登录,很麻烦,

    /**
     * 跳转到商品列表页面
//     * @param request
//     * @param response
     * @param model
//     * @param ticket
     * @return
     */
    @RequestMapping("/toList")
    public String toList(HttpServletRequest request, HttpServletResponse response, Model model, @CookieValue("userTicket") String ticket){
        if(StringUtils.isEmpty(ticket)){
            return "login";
        }
//        User user = (User)session.getAttribute(ticket);
        User user = userService.getUserByCookie(ticket, request, response);                      if(null == user){
            return "login";
        }
        model.addAttribute("user", user);
        return "goodsList";
    }

接口优化后,将用户有没有登录这个判断放到参数判断里面。

    public String toList(Model model, User user){

        model.addAttribute("user", user);
        return "goodsList";
    }

5.3.3.1 WebConfig


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    UserArgumentResolver userArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }
}

5.3.3.2 HandlerMethodArgumentResolver

import com.xxx.seckill.pojo.User;
import com.xxx.seckill.service.IUserService;
import com.xxx.seckill.util.CookieUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.thymeleaf.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义用户参数
 */

@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private IUserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> clazz = parameter.getParameterType();
        return clazz == User.class;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);

        String ticket = CookieUtil.getCookieValue(request, "userTicket");
        if(StringUtils.isEmpty(ticket)){
            return null;
        }

        return userService.getUserByCookie(ticket, request, response);
    }
}

6 秒杀功能

6.1 商品列表页

6.1.1 逆向功能生成所需所有类

数据库所需相关SQL语句

商品表t_goods

CREATE TABLE `t_goods`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名称',
`goods_title` VARCHAR(64) DEFAULT NULL COMMENT '商品标题',
`goods_img` VARCHAR(64) DEFAULT NULL COMMENT '商品图片',
`goods_detail` LONGTEXT COMMENT '商品详情',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品价格',
`goods_stock` INT(11) DEFAULT '0' COMMENT '商品库存, -1表示没有限制',
PRIMARY KEY(`id`)
)ENGINE = INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

订单表t_order

CREATE TABLE `t_order`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID',
`delivery_addr_id` BIGINT(20) DEFAULT NULL COMMENT '收货地址ID',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '冗余过来的商品名称',
`goods_count` INT(11) DEFAULT '0' COMMENT '商品数量',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价',
`order_channel` TINYINT(4) DEFAULT '0' COMMENT '1 pc, 2 android, 3 ios',
`status` TINYINT(4) DEFAULT '0' COMMENT '订单状态,0新创建未支付,1已支付,2已发货,3已收货,4已退款,5已完成',
`create_date` datetime DEFAULT NULL COMMENT '订单的创建时间',
`pay_date` datetime DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY(`id`)
)ENGINE = INNODB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;

秒杀商品表t_seckill_goods

CREATE TABLE `t_seckill_goods`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID',
`seckill_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '秒杀价',
`stock_count` INT(10) DEFAULT NULL COMMENT '库存数量',
`start_date` datetime DEFAULT NULL COMMENT '秒杀开始时间',
`end_date` datetime DEFAULT NULL COMMENT '秒杀结束时间',
PRIMARY KEY(`id`)
)ENGINE = INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

秒杀订单表t_seckill_order

CREATE TABLE `t_seckill_order`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀订单ID',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID',
`order_id` BIGINT(20) DEFAULT NULL COMMENT '订单ID',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID',
PRIMARY KEY(`id`)
)ENGINE = INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

6.1.2 HTML

<!DOCTYPE html>
<html lang="en"
        xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品列表</title>
    <script type="text/javascript" th:src="@{/js/jquery.min.jp}"></script>
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <script type="text/javascript" th: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>商品图片</td>
                <td>商品原价</td>
                <td>秒杀价</td>
                <td>库存数量</td>
                <td>详情</td>
            </tr>
            <tr th:each="goods,goodsStat : ${goodsList}">
                <td th:text="${goods.goodsName}"></td>
                <td><img th:src="@{${goods.goodsImg}}" width="100" height="100"/></td>
                <td th:text="${goods.goodsPrice}"></td>
                <td th:text="${goods.seckillPrice}"></td>
                <td th:text="${goods.stockCount}"></td>
                <td><a th:href="'/goods/toDetail/' + ${goods.id}">详情</a></td>
            </tr>
        </table>
    </div>

</body>
</html>

6.1.3 MvcConfig

如果出现图片和其他静态资源加载不出来,需要修改配置类。

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }

将static下文件目录,配置成为静态资源配置目录。

6.1.4 GoodsController

    @RequestMapping("/toList")
    public String toList(Model model, User user){

        model.addAttribute("user", user);

        model.addAttribute("goodsList", goodsService.findGoodsVo());

        return "goodsList";
    }

6.1.5 GoodsServiceImpl

@Override
public List<GoodsVo> findGoodsVo() {

    return goodsMapper.findGoodsVo();
}

6.1.6 GoodsMapper

public interface GoodsMapper extends BaseMapper<Goods> {

   /**
    * 查询商品列表
    * @return
    */
   List<GoodsVo> findGoodsVo();

}
<!--<?xml version="1.0" encoding="UTF-8"?>-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.seckill.mapper.GoodsMapper">

<!--    通用查询映射结果-->
    <resultMap id="BaseResultMap" type="com.xxx.seckill.pojo.Goods">
        <id column="id" property="id"/>
        <result column="goods_name" property="goodsName"/>
        <result column="goods_title" property="goodsTitle"/>
        <result column="goods_img" property="goodsImg"/>
        <result column="goods_detail" property="goodsDetail"/>
        <result column="goods_price" property="goodsPrice"/>
        <result column="goods_stock" property="goodsStock"/>
    </resultMap>

<!--    通用查询结果列-->
    <sql id="Base_Column_List">
        id, goods_name, goods_title, goods_img, goods_detail, goods_price, goods_stock
    </sql>

<!--    获取商品列表-->
    <select id="findGoodsVo" resultType="com.xxx.seckill.vo.GoodsVo">
        SELECT
            g.id,
            g.goods_name,
            g.goods_title,
            g.goods_img,
            g.goods_detail,
            g.goods_price,
            g.goods_stock,
            sg.seckill_price,
            sg.stock_count,
            sg.start_date,
            sg.end_date
        FROM
        t_goods g
        LEFT JOIN t_seckill_goods as sg on g.id = sg.goods_id;
    </select>

</mapper>

6.1.7 GoodsVo

商品视图类继承商品类


@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsVo extends Goods {

    private BigDecimal seckillPrice;
    private Integer stockCount;
    private Date startDate;
    private Date endDate;

}

6.1.8 测试

image.png

6.2 商品详情页

6.2.1 GoodsMapper

    /**
     * 获取商品详情
     * @param goodsId
     * @return
     */
    GoodsVo findGoodsVoByGoodsId(Long goodsId);
<!--    获取商品详情-->
    <select id="findGoodsVoByGoodsId" resultType="com.xxx.seckill.vo.GoodsVo">
        SELECT
            g.id,
            g.goods_name,
            g.goods_title,
            g.goods_img,
            g.goods_detail,
            g.goods_price,
            g.goods_stock,
            sg.seckill_price,
            sg.stock_count,
            sg.start_date,
            sg.end_date
        FROM
        t_goods g
        LEFT JOIN t_seckill_goods as sg on g.id = sg.goods_id
        WHERE
            g.id = #{goodsId};
    </select>

6.2.2 GoodsService

    /**
     * 功能描述:获取指定商品详情信息
     * @param goodsId
     * @return
     */
    @Override
    public GoodsVo findGoodsVoByGoodsId(Long goodsId) {
        return goodsMapper.findGoodsVoByGoodsId(goodsId);
    }

6.2.3 GoodsController


    /**
     * 功能描述:获取商品详情
     * @param model
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping("/toDetail/{goodsId}")
    public String toList(Model model, User user, @PathVariable Long goodsId){

        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date nowDate = new Date();
        //秒杀状态
        int secKillStatus = 0;
        //秒杀倒计时
        int remainSeconds = 0;
        //秒杀还未开始
        if(nowDate.before(startDate)){
            remainSeconds = (int)((startDate.getTime() - nowDate.getTime()) / 1000);
        }else if(nowDate.after(endDate)){
            //秒杀已结束
            secKillStatus = 2;
            remainSeconds = -1;
        }else {
            secKillStatus = 1;
            remainSeconds = 0;
        }
        model.addAttribute("remainSeconds", remainSeconds);
        model.addAttribute("secKillStatus", secKillStatus);
        model.addAttribute("goods", goodsVo);
        return "goodsDetail";
    }

6.2.4 goodsDetail.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品列表</title>
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>

    <div class="panel panel-default">
        <div class="panel-heading">秒杀商品详情</div>
        <div class="panel-body">
            <span th:if="${user eq null}">您还没有登录,请登录后在操作<br/></span>
            <span>没有收货地址的提示...</span>
        </div>
        <table class="table" id="goods">
            <tr>
                <td>商品名称</td>
                <td colspan="3" th:text="${goods.goodsName}"></td>
            </tr>
            <tr>
                <td>商品图片</td>
                <td colspan="3"><img th:src="@{${goods.goodsImg}}" width="200" height="200"/> </td>
            </tr>
            <tr>
                <td>秒杀开始时间</td>
                <td th:text="${#dates.format(goods.startDate, 'yyyy-MM-dd HH:mm:ss')}"></td>
                <td id="seckillTip">
                    <input type="hidden" id="remainSeconds" th:value="${remainSeconds}"/>
                    <span th:if="${secKillStatus eq 0}">秒杀倒计时:<span id="countDown" th:text="${remainSeconds}"></span>秒</span>
                    <span th:if="${secKillStatus eq 1}">秒杀进行中</span>
                    <span th:if="${secKillStatus eq 2}">秒杀已结束</span>
                </td>
                <td>
                    <form id="secKillForm" method="post" action="/secKill/doSecKill">
                        <input type="hidden" name="goodsId" th:value="${goods.id}"/>
                        <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
                    </form>
                </td>
            </tr>
            <tr>
                <td>商品原价</td>
                <td colspan="3" th:text="${goods.goodsPrice}"></td>
            </tr>
            <tr>
                <td>秒杀价</td>
                <td colspan="3" th:text="${goods.seckillPrice}"></td>
            </tr>
            <tr>
                <td>库存数量</td>
                <td colspan="3" th:text="${goods.stockCount}"></td>
            </tr>
        </table>

    </div>

</body>
<script>
    $(function(){
       countDown();
    });

    function countDown(){
        var remainSeconds = $("#remainSeconds").val();
        var timeout;
        //秒杀还未开始
        if(remainSeconds > 0){
            $("#buyButton").attr("disabled", true);
            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);
            }
            $("#seckillTip").html("秒杀进行中");
        }else{
            $("#buyButton").attr("disabled", true);
            $("#seckillTip").html("秒杀已经结束");
        }
    };

</script>
</html>

6.2.5 测试

image.png
image.png
image.png

6.3 秒杀功能实现

6.3.1 SeckillController

@Controller
@RequestMapping("/secKill")
public class SecKillController {

    @Autowired
    private IGoodsService goodsService;

    @Autowired
    private ISeckillOrderService seckillOrderService;

    @Autowired
    private IOrderService orderService;

    /**
     * 功能描述:秒杀
     * @param model
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping("/doSecKill")
    public String doSecKill(Model model, User user, Long goodsId){

        if(user == null){
            return "login";
        }
        model.addAttribute("user", user);
        GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
        //判断库存
        if(goods.getStockCount() < 1){
            model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
            return "secKillFail";
        }
        //判断是否重复抢购
        SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));

        if(seckillOrder != null){
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }
        Order order = orderService.secKill(user, goods);
        model.addAttribute("order", order);
        model.addAttribute("goods", goods);
        return "orderDetail";
    }

}

6.3.2 OrderServiceImpl

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {

    @Autowired
    private ISeckillOrderService seckillOrderService;

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private ISeckillGoodsService seckillGoodsService;

    /**
     * 功能描述:秒杀
     * @param user
     * @param goods
     * @return
     */
    @Override
    public Order secKill(User user, GoodsVo goods) {
        //秒杀商品表减库存
        SeckillGoods seckillGoods = seckillGoodsService.getOne(new QueryWrapper<SeckillGoods>().eq("goods_id", goods.getId()));
        seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
        seckillGoodsService.updateById(seckillGoods);
        //生成订单
        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.setUserId(user.getId());
        seckillOrder.setOrderId(order.getId());
        seckillOrder.setGoodsId(goods.getId());
        seckillOrderService.save(seckillOrder);
        return order;
    }
}

6.4 订单详情页

6.4.1 OrderDetail.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>订单详情</title>
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
    <div class="panel panel-default">
        <div class="panel-heading">秒杀订单详情</div>
        <table class="table" id="order">
            <tr>
                <td>商品名称</td>
                <td th:text="${goods.goodsName}" colspan="3"> </td>
            </tr>
            <tr>
                <td>商品图片</td>
                <td colspan="2"><img th:src="@{${goods.goodsImg}}" width="200"  height="200"/> </td>
            </tr>
            <tr>
                <td>订单价格</td>
                <td colspan="2" th:text="${order.goodsPrice}"></td>
            </tr>
            <tr>
                <td>下单时间</td>
                <td th:text="${#dates.format(order.createDate, 'yyyy-MM-dd HH:mm:ss')}" colspan="2"></td>
            </tr>
            <tr>
                <td>订单状态</td>
                <td>
                    <span th:if="${order.status eq 0}">未支付</span>
                    <span th:if="${order.status eq 1}">待发货</span>
                    <span th:if="${order.status eq 2}">已发货</span>
                    <span th:if="${order.status eq 3}">已收货</span>
                    <span th:if="${order.status eq 4}">已退款</span>
                    <span th:if="${order.status eq 5}">已完成</span>
                </td>
                <td>
                    <button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button>
                </td>
            </tr>
            <tr>
                <td>收货人</td>
                <td colspan="2">xxx 18012345678</td>
            </tr>
            <tr>
                <td>收货地址</td>
                <td colspan="2">上海市静安区静安寺</td>
            </tr>
        </table>
    </div>
</body>

</html>

6.4.2 测试

image.png

7 系统压测

7.1 JMeter入门

7.1.1 安装

省略

7.1.2 简单使用

7.1.2.1 创建线程组

步骤:添加--->线程(用户)--->线程组


image.png

7.1.2.2 创建HTTP请求默认值

步骤:添加--->配置原件--->HTTP请求默认值


image.png

7.1.2.3 创建HTTP请求

步骤:添加--->取样器--->HTTP请求


image.png

7.1.2.4 聚合报告

步骤:添加--->监听器--->聚合报告


image.png

7.1.2.5 图形结果

步骤:添加--->监听器--->图形结果


image.png

7.1.2.6 查看结果树

步骤:添加--->监听器--->查看结果树


image.png

7.1.3 Linux使用JMeter

7.1.3.1 打jar包,发送到Linux运行程序

java -jar SecKill.jar
注意:打包时注意配置文件MySQL和Redis的服务器IP

7.1.3.2 安装JMeter

tar -zxvf JMeter.tar -C /usr/local/

7.1.3.3 修改jmeter.properties

language=zh_CN
sampleresult.default.encoding=UTF-8

7.1.3.4 将Window执行的测试计划生成文件first.jmx,并发送到Linux

7.1.3.5 无图形界面执行JMeter程序

命令: ./jmeter.sh -n -t first.jmx -l result.jtl

image.png

7.1.3.6 top监控Liunx的状态

top命令可以看到当前CPU资源的使用情况,已经服务器的负载情况。

测试前


image.png

压测时


image.png

结束前
image.png

7.1.3.7 将生成的结果文件放到window图形界面的客户端查看

image.png

7.1.4 自定义变量

7.1.4.1 准备测试接口

@Controller
@RequestMapping("/user")
public class UserController {

    /**
     * 功能描述:压力测试接口
     */
    @RequestMapping("/info")
    @ResponseBody
    public RespBean info(User user){
        return RespBean.success(user);
    }

}

7.1.4.2 未登录状态测试

image.png

7.1.4.3 配置同一用户进行测试

image.png

image.png

7.1.4.4 配置不同用户进行测试

config.txt


image.png

用户信息


image.png

CSV数据文件设置


image.png

HTTP Cookie管理器


image.png

测试结果


image.png

7.1.5 正式压测

7.1.5.1 创建用户

package com.xxx.seckill.util;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.seckill.pojo.User;
import com.xxx.seckill.vo.RespBean;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.sql.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 生成用户工具类
 *
 */

public class UserUtil {
    private static void createUser(int count) throws Exception {
        List<User> users = new ArrayList<>(count);

        for(int i = 0; i < count; i++){
            User user = new User();
            user.setId(13000000000L + i);
            user.setNickname("user" + i);
            user.setSlat("1a2b3c4d");
            user.setLoginCount(1);
            user.setRegisterDate(new Date());
            user.setPassword(MD5Util.inputPassToDBPass("123456", user.getSlat()));
            users.add(user);
        }
        System.out.println("create user");

//        //插入数据库
//        Connection conn = getConn();
//        String sql = "insert into t_user(login_count, nickname, register_date, slat, password, id) values(?,?,?,?,?,?)";
//
//        PreparedStatement pstmt = conn.prepareStatement(sql);
//        for(int i = 0; i < users.size(); i++){
//            User user = users.get(i);
//            pstmt.setInt(1, user.getLoginCount());
//            pstmt.setString(2, user.getNickname());
//            pstmt.setTimestamp(3, new Timestamp(user.getRegisterDate().getTime()));
//            pstmt.setString(4, user.getSlat());
//            pstmt.setString(5, user.getPassword());
//            pstmt.setLong(6, user.getId());
//            pstmt.addBatch();
//        }
//
//        pstmt.executeBatch();
//        pstmt.clearParameters();
//        conn.close();
        System.out.println("insert to do");


        //登录,生成UserTicket
        String urlString = "http://localhost:8080/login/doLogin";
        File file = new File("C:\\Users\\water\\Desktop\\config.txt");
        if(file.exists()){
            file.delete();
        }
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        raf.seek(0);
        for(int i = 0; i < users.size(); i++){
            User user = users.get(i);
            URL url = new URL(urlString);
            HttpURLConnection co = (HttpURLConnection) url.openConnection();
            co.setRequestMethod("POST");
            co.setDoOutput(true);
            OutputStream out = co.getOutputStream();
            String params = "mobile=" + user.getId() + "&password="+MD5Util.inputPassToFromPass("123456");
            out.write(params.getBytes());
            out.flush();
            InputStream inputStream = co.getInputStream();
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            byte[] buff = new byte[1024];
            int len = 0;
            while((len = inputStream.read(buff)) >= 0){
                bout.write(buff, 0, len);
            }
            inputStream.close();
            bout.close();
            String response = new String(bout.toByteArray());
            ObjectMapper mapper = new ObjectMapper();
            RespBean respBean = mapper.readValue(response, RespBean.class);
            String userTicket = (String) respBean.getObj();
            System.out.println("create userTicket : " + user.getId());
            String row = user.getId() + "," + userTicket;
            raf.seek(raf.length());
            raf.write(row.getBytes());
            raf.write("\r\n".getBytes());
            System.out.println("write to file :" + user.getId());
        }
        raf.close();
        System.out.println("over");

    }

    private static Connection getConn() throws ClassNotFoundException, SQLException {
        String url = "jdbc:mysql:///seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai";
        String username = "root";
        String password = "123";
        String driver = "com.mysql.cj.jdbc.Driver";
        Class.forName(driver);
        return DriverManager.getConnection(url, username, password);

    }


    public static void main(String[] args) throws Exception {
        createUser(5000);
    }
}

7.1.5.2 config.txt

13000000000,c392864b95bb4703bf95f3314b0f152a
13000000001,41ac0f9a1c4a42299f0f7dbcdeb0c059
13000000002,3083ad7fe10245c686c59a6f4531af9d
13000000003,3d152ba7316046ea85515f057accf6f7
13000000004,32cebb19a336444295db1227561c4e26
13000000005,19c0e798b1eb476ea070417d98bac4fc
13000000006,fb3357370fbb424393a4412c73645b2b
13000000007,fb475716ed2b403a913c88fabfbc485c
13000000008,70dd1b2f47bf4cb6bacd2f55c12a403e
13000000009,2bc51f980ec14c23bf1dca993d4e19ca
13000000010,fea595f766b24a3d9b366d2d78cabea9
13000000011,9b1a9fe8d8bc4c96b405e5391c7dd196
13000000012,c104bdc765a64ac0b7ef49064ce60423
13000000013,ae13ee5a7c5d482a85674526f430e35e
13000000014,2fa6851398574dc78569e03f3918568f
13000000015,91499670e73747c7aa24fd2f2c69af76
.....

7.1.5.3 配置秒杀接口测试

  • 线程组


    image.png
  • HTTP请求默认值


    image.png
  • CVS数据文件设置


    image.png
  • HTTP Cookie管理器


    image.png
  • HTTP请求


    image.png
  • 结果


    image.png
image.png
image.png

注意:数据库中存在商品超卖的问题。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容