5.5 分布式Session
5.5.1 分布式Session问题
之前的代码在一台服务器上运行服务没有问题,当部署多太服务器系统,配合Nginx的时候会出现用户登录的问题。每台服务器都会存储一个Session,不同服务器的Session是不共用的。
原因:由于Nginx使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一发到后端的应用上。
刚开始我们在Tomcat1登录之后,用户信息放在Tomcat1的Session里。过一会,请求又被Nginx分发到Tomcat2上,Tomcat2上Session里还没有用户信息,于是又要登录。
解决方案
- 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 测试
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 测试
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 测试
7 系统压测
7.1 JMeter入门
7.1.1 安装
省略
7.1.2 简单使用
7.1.2.1 创建线程组
步骤:添加--->线程(用户)--->线程组
7.1.2.2 创建HTTP请求默认值
步骤:添加--->配置原件--->HTTP请求默认值
7.1.2.3 创建HTTP请求
步骤:添加--->取样器--->HTTP请求
7.1.2.4 聚合报告
步骤:添加--->监听器--->聚合报告
7.1.2.5 图形结果
步骤:添加--->监听器--->图形结果
7.1.2.6 查看结果树
步骤:添加--->监听器--->查看结果树
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
7.1.3.6 top监控Liunx的状态
top命令可以看到当前CPU资源的使用情况,已经服务器的负载情况。
测试前
image.png
压测时
image.png
结束前
image.png
7.1.3.7 将生成的结果文件放到window图形界面的客户端查看
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 未登录状态测试
7.1.4.3 配置同一用户进行测试
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
注意:数据库中存在商品超卖的问题。