目录
- 一、生成测试用户
- 二、jmeter压测
- 三、秒杀接口优化
- 1、优化第一步:解决超卖
- 2、优化第二步:Redis重复抢购
- 3、优化第三步:Redis预减库存
- ①商品初始化
- ②预减库存
一、生成测试用户
将UserUtils工具类导入到zmall-user模块中,运行生成测试用户信息,可根据自身电脑情况来生成用户数量。
1)必须保证zmall-user模块处于运行状态下,在进行测试用户数据生成操作;
2)注意修改UserUtils中的用户登录接口地址及端口;同时请修改用户登录接口,将生成的token令牌存入响应封装类中;//5.通过UUID生成token令牌并保存到cookie中 String token= UUID.randomUUID().toString().replace("-",""); ... return new JsonResponseBody<>(token);
3)设置生成登录令牌存储位置;
4)修改数据库名、登录账号及密码;
5)设置生成测试用户数量;
UserUtils
package com.zking.zmall.utils;
import com.alibaba.nacos.common.utils.MD5Utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zking.zmall.model.User;
import com.zking.zmall.util.JsonResponseBody;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class UserUtils {
private static void createUser(int count) throws Exception {
List<User> lst=new ArrayList<User>();
//循环添加用户数据
for(int i=0;i<count;i++){
User user=new User();
user.setLoginName("user"+i);
user.setUserName("测试用户"+i);
user.setPassword(MD5Utils.md5Hex("123456".getBytes()));
user.setType(0);
user.setMobile((17700000000L+i)+"");
user.setEmail("user"+i+"@139.com");
user.setIdentityCode((430104199912120000L+i)+"");
lst.add(user);
}
System.out.println("create users");
//获取数据库连接
Connection conn=getConn();
//定义SQL
String sql="insert into zmall_user(loginName,userName,password,identityCode,email,mobile,type) values(?,?,?,?,?,?,?)";
//执行SQL
PreparedStatement ps=conn.prepareStatement(sql);
//赋值
for (User user : lst){
ps.setString(1,user.getLoginName());
ps.setString(2,user.getUserName());
ps.setString(3,user.getPassword());
ps.setString(4,user.getIdentityCode());
ps.setString(5,user.getEmail());
ps.setString(6,user.getMobile());
ps.setInt(7,user.getType());
ps.addBatch();
}
ps.executeBatch();
ps.clearParameters();
ps.close();
conn.close();
System.out.println("insert to db");
//登录,生成UserTicket
String urlString="http://localhost:8010/userLogin";
File file=new File("C:\\Users\\Administrator\\DeskTop\\config.txt");
if(file.exists()){
file.delete();
}
RandomAccessFile accessFile=new RandomAccessFile(file,"rw");
//设置光标位置
accessFile.seek(0);
for (User user : lst) {
URL url=new URL(urlString);
HttpURLConnection co = (HttpURLConnection) url.openConnection();
co.setRequestMethod("POST");
co.setDoOutput(true);
OutputStream out=co.getOutputStream();
String params="loginName="+user.getLoginName()+"&password=123456";
out.write(params.getBytes());
out.flush();
InputStream in=co.getInputStream();
ByteArrayOutputStream bout=new ByteArrayOutputStream();
byte[] buffer=new byte[1024];
int len=0;
while((len=in.read(buffer))>=0){
bout.write(buffer,0,len);
}
in.close();
bout.close();
String response=new String(bout.toByteArray());
ObjectMapper mapper=new ObjectMapper();
JsonResponseBody jsonResponseBody=mapper.readValue(response, JsonResponseBody.class);
String token=jsonResponseBody.getData().toString();
System.out.println("create token:"+token);
accessFile.seek(accessFile.length());
accessFile.write(token.getBytes());
accessFile.write("\r\n".getBytes());
//System.out.println("write to file:"+token);
}
accessFile.close();
System.out.println("over");
}
private static Connection getConn() throws Exception {
String url="jdbc:mysql://localhost:3306/zmall?useSSL=false&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&characterEncoding=UTF8";
String driver="com.mysql.jdbc.Driver";
String username="root";
String password="123456";
Class.forName(driver);
return DriverManager.getConnection(url,username,password);
}
public static void main(String[] args) throws Exception {
createUser(50);
}
}
二、jmeter压测
相关配置
1.线程计划>添加>线程(用户)>线程组
2.线程组>添加>配置元件>http请求默认值
3.线程组>添加>取样器>http请求
4.线程组>添加>配置元件>http cookie管理器
5.线程组>添加>配置元件>CSV数据文件设置
6.线程组>添加>监听器>汇总报告
7.线程组>添加>监听器>查看结果树
8.线程组>添加>监听器>用表格查看结果
线程组:200个线程,1秒之内发送,循环1次。测试结果如下:吞吐量为1328/s
数据库中的秒杀商品表中的商品出现了库存为负数的问题。
订单表和订单项表中出现了秒杀商品超卖问题。
三、秒杀接口优化
1、优化第一步:解决超卖
更新秒杀商品库存的sql语句,只有当库存大于0才能更新库存;修改更新秒杀库存方法updateKillStockById的返回类型为boolean,用于判断是否更新成功。
OrderServiceImpl
@Transactional
@Override
public JsonResponseBody<?> createKillOrder(User user, Integer pid, Float price) {
//1.根据秒杀商品编号获取秒杀商品库存是否为空
//........
//2.秒杀商品库存减一
boolean update = killService.update(new UpdateWrapper<Kill>()
.eq("item_id", pid)
.gt("total", 0)
.setSql("total=total-1"));
if(!update)
throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
//3.生成秒杀订单及订单项
//........
return new JsonResponseBody();
}
2、优化第二步:Redis重复抢购
在RedisService中新增以下两个方法,用于Redis重复抢购的判断操作。
- 根据用户ID和秒杀商品ID为Key,将秒杀订单保存到Redis中;
- 根据用户ID和秒杀商品ID从Redis中获取对应的秒杀商品;
RedisServiceImpl
/**
* 将秒杀订单保存到Redis
* @param pid 商品ID
* @param order 秒杀订单
*/
@Override
public void setKillOrderToRedis(Integer pid, Order order) {
redisTemplate.opsForValue().set("order:"+order.getUserId()+":"+pid,order,1800, TimeUnit.SECONDS);
}
/**
* 根据用户ID和商品ID从Redis中获取秒杀商品,用于重复抢购判断
* @param uid 用户ID
* @param pid 商品ID
* @return 返回Redis中存储的秒杀订单
*/
@Override
public Order getKillOrderByUidAndPid(Integer uid, Integer pid) {
return (Order) redisTemplate.opsForValue().get("order:"+uid+":"+pid);
}
这里用户抢购的秒杀订单保存到Redis默认设置是1800秒,即30分钟;可视情况具体调整。
OrderServiceImpl
@Transactional
@Override
public JsonResponseBody<?> createKillOrder(User user, Integer pid) {
...
/***********在库存判断是否为空之后***********/
//6.根据秒杀商品ID和用户ID判断是否重复抢购
Order order = redisService.getKillOrderByUidAndPid(user.getId(), pid);
if(null!=order)
throw new BusinessException(JsonResponseStatus.ORDER_REPART);
/***********在根据商品ID获取商品之前***********/
//4.秒杀商品库存减一
boolean flag=killService.updateKillStockById(pid);
if(!flag)
throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
...
//生成秒杀订单等操作
//重点,重点,重点,在此处将生成的秒杀订单保存到Redis中,用于之后的重复抢购判断
redisService.setKillOrderToRedis(pid,order);
return new JsonResponseBody<>();
}
此处第二步优化完毕,再次进行JMeter压测,并查看测试情况。
3、优化第三步:Redis预减库存
①商品初始化
将参与秒杀活动且秒杀状态、秒杀活动时间有效的商品推送到Redis中,并对秒杀商品设置超时时间。
超时时间的设定取至于活动结束时间减去活动开始时间的差值,但必须是有效活动时间,也就是当前时间在活动开始时间与结束时间范围之内。
IRedisService
/**
* 设置秒杀商品库存到Redis中
* @param pid 秒杀商品ID
* @param total 秒杀商品数量
* @param expires 秒杀商品存储过期时间
*/
void setKillTotaltoRedis(Integer pid,Integer total,long expires);
RedisServiceImpl
@Override
public void setKillTotaltoRedis(Integer pid, Integer total,long expires) {
redisTemplate.opsForValue().set("goods:"+pid,total,expires,TimeUnit.DAYS);
}
OrderController
在zmall-order订单模块中的OrderController类上实现InitializingBean,完成秒杀商品预加载。
package com.zking.zmall.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zking.zmall.model.Kill;
import com.zking.zmall.model.Order;
import com.zking.zmall.model.User;
import com.zking.zmall.service.IOrderService;
import com.zking.zmall.service.impl.KillServiceImpl;
import com.zking.zmall.service.impl.RedisServiceImpl;
import com.zking.zmall.util.JsonResponseBody;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.List;
@Controller
public class OrderController implements InitializingBean {
@Autowired
private IOrderService orderService;
@Autowired
private KillServiceImpl killService;
@Autowired
private RedisServiceImpl redisService;
/**
* 秒杀商品初始化
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
List<Kill> list =killService.list(new QueryWrapper<Kill>()
//秒杀活动必须是激活状态
.eq("is_active", 1)
//秒杀活动结束时间必须>=当前时间,小于证明活动已结束
.ge("end_time",new Date().toLocaleString()));
list.forEach(kill -> {
//计算秒杀商品存入Redis的过期时间,此处以天为单位
Instant start = kill.getStartTime().toInstant();
Instant end = kill.getEndTime().toInstant();
long days = Duration.between(start, end).toDays();
redisService.setKillTotaltoRedis(kill.getItemId(),kill.getTotal(),days);
});
}
@RequestMapping("/orderUserList")
@ResponseBody
public List<Order> orderUserList(){
return orderService.list(new QueryWrapper<Order>()
.eq("userId",18));
}
@RequestMapping("/createOrder/{pid}/{num}")
@ResponseBody
public Order createOrder(@PathVariable("pid") Integer pid,
@PathVariable("num") Integer num){
return orderService.createOrder(pid,num);
}
@RequestMapping("/createKillOrder/{pid}/{price}")
@ResponseBody
public JsonResponseBody<?> createKillOrder(User user,
@PathVariable("pid") Integer pid,
@PathVariable("price") Float price){
return orderService.createKillOrder(user,pid,price);
}
}
②预减库存
第一步:在RedisService中定义库存预减和递增方法。预减方法是在用户抢购商品成功后对商品进行库存预减;递增方法是在高并发情况下Redis库存预减可能会出现负数情况,通过递增方法进行库存回滚为0
IRedisService
/**
* 根据秒杀商品ID实现Redis商品库存递增
* @param pid
* @return
*/
long increment(Integer pid);
/**
* 根据秒杀商品ID实现Redis商品库存递减
* @param pid
* @return
*/
long decrement(Integer pid);
RedisServiceImpl
@Override
public long increment(Integer pid) {
return redisTemplate.opsForValue().increment("goods:"+pid);
}
@Override
public long decrement(Integer pid) {
return redisTemplate.opsForValue().decrement("goods:"+pid);
}
第二步:修改订单生成方法,加入Redis库存预减判断
请在Redis重复抢购判断的下面加入Redis库存预减操作。
OrderServiceImpl
package com.zking.zmall.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zking.zmall.exception.BusinessException;
import com.zking.zmall.mapper.OrderMapper;
import com.zking.zmall.model.Kill;
import com.zking.zmall.model.Order;
import com.zking.zmall.model.OrderDetail;
import com.zking.zmall.model.User;
import com.zking.zmall.service.ApiProductService;
import com.zking.zmall.service.IOrderService;
//import io.seata.spring.annotation.GlobalTransactional;
import com.zking.zmall.util.JsonResponseBody;
import com.zking.zmall.util.JsonResponseStatus;
import com.zking.zmall.util.SnowFlake;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* <p>
* 服务实现类
* </p>
*
* @author xnx
* @since 2023-02-06
*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
@Autowired
private KillServiceImpl killService;
@Autowired
private OrderDetailServiceImpl orderDetailService;
@Autowired
private ApiProductService productService;
@Autowired
private RedisServiceImpl redisService;
// @Transactional
// @Override
// public Order createOrder(Integer pid, Integer num) {
// //根据商品ID修改商品对应的库存
// productService.updateStock(pid,num);
// //新增订单
// Order order=new Order();
// //此处只是做模拟操作
// this.save(order);
// return order;
// }
// @GlobalTransactional
@Transactional
@Override
public Order createOrder(Integer pid, Integer num) {
//根据商品ID修改商品对应的库存
productService.updateStock(pid,num);
//异常模拟
int i = 1 / 0;
//新增订单
Order order=new Order();
//此处只是做模拟操作
this.save(order);
return order;
}
@Transactional
@Override
public JsonResponseBody<?> createKillOrder(User user, Integer pid, Float price) {
//1.根据秒杀商品编号获取秒杀商品库存是否为空
// Kill kill = killService.getOne(new QueryWrapper<Kill>().eq("item_id",pid));
// if(kill.getTotal()<1)
// throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
//2.秒杀商品库存减一
// killService.update(new UpdateWrapper<Kill>()
// .eq("item_id",pid)
// .setSql("total=total-1"));
/***********在库存判断是否为空之后***********/
//6.Redis库存预减
long stock = redisService.decrement(pid);
if(stock<0){
redisService.increment(pid);
throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
}
//5.根据秒杀商品ID和用户ID判断是否重复抢购
Order order = redisService.getKillOrderByUidAndPid(user.getId(), pid);
if(null!=order)
throw new BusinessException(JsonResponseStatus.ORDER_REPART);
//2.秒杀商品库存减一
boolean update = killService.update(new UpdateWrapper<Kill>()
.eq("item_id", pid)
.gt("total", 0)
.setSql("total=total-1"));
if(!update)
throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
// boolean flag=killService.updateKillStockById(pid);
// if(!flag)
// throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
//3.生成秒杀订单及订单项
SnowFlake snowFlake=new SnowFlake(2,3);
Long orderId=snowFlake.nextId();
int orderIdInt = new Long(orderId).intValue();
//创建订单
// Order order=new Order();
order.setUserId(user.getId());
order.setLoginName(user.getLoginName());
order.setCost(price);
order.setSerialNumber(orderIdInt+"");
this.save(order);
//创建订单项
OrderDetail orderDetail=new OrderDetail();
orderDetail.setOrderId(orderIdInt);
orderDetail.setProductId(pid);
orderDetail.setQuantity(1);
orderDetail.setCost(price);
orderDetailService.save(orderDetail);
//生成秒杀订单等操作
//重点,重点,重点,在此处将生成的秒杀订单保存到Redis中,用于之后的重复抢购判断
redisService.setKillOrderToRedis(pid,order);
return new JsonResponseBody();
}
}
第三步:还原测试数据,重新使用jmeter压测,这时可以发现明显压测效率要提升很多。
但是还是要根据不同电脑配置情况来决定,配置太低,效率也提升不了多少。
尤其是链接远程redis,会导致压测的吞吐量直线下降