概述
目录
一、生成测试用户
二、jmeter压测
测试压测1
三、秒杀接口优化
1.优化第一步:解决超卖问题
测试压测2
2.优化第二步:Redis重复抢购问题
3.优化第三步:Redis的预减库存问题
① 商品初始化
② 预减库存
压测3
一、生成测试用户
将UserUtils工具类导入到zmall-user模块中,运行生成测试用户信息,可根据自身电脑情况来生成用户数量。
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\zjjt\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("rn".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(100);
}
}
⑴ 必须保证zmall-user模块处于运行状态下,在进行测试用户数据生成操作;
⑵ 注意修改UserUtils中的用户登录接口地址及端口;同时请修改用户登录接口,将生成的token令牌存入响应封装类中;
//5.通过UUID生成token令牌并保存到cookie中 String token= UUID.randomUUID().toString().replace("-",""); ... return new JsonResponseBody<>(token);
⑶ 设置生成登录令牌存储位置;
⑷ 修改数据库名,登录账户及密码;
⑸ 设置生成测试用户数量;
首先把MySQL打开把所有的数据清除一下,确保不必要的数据。
JMeter压测脚本调测.sql
SELECT * FROM zmall_kill;
SELECT * FROM zmall_order;
SELECT * FROM zmall_order_detail;
SELECT * FROM zmall_user;
-- 数据库秒杀数据初始化
DELETE FROM zmall_user WHERE loginName like 'user%';
TRUNCATE table zmall_order;
truncate table zmall_order_detail;
update zmall_kill set total = 10;
生成测试用户,前提是nacos要开启以及zmall-user启动,Jmeter也要开启。
运行我们UserUtils的main方法,生成我们的token令牌
二、jmeter压测
相关配置
1.线程计划>添加>线程(用户)>线程组
2.线程组>添加>配置元件>http请求默认值
3.线程组>添加>取样器>http请求
4.线程组>添加>配置元件>http cookie管理器
5.线程组>添加>配置元件>CSV数据文件设置
6.线程组>添加>监听器>汇总报告
7.线程组>添加>监听器>查看结果树
8.线程组>添加>监听器>用表格查看结果
线程组 (线程计划>添加>线程(用户)>线程组)
保存到我们桌面里面去,如下图所示:
http请求默认值 (线程组>添加>配置元件>http请求默认值)
http请求 (线程组>添加>取样器>http请求)
CSV数据文件设置(线程组>添加>配置元件>CSV数据文件设置)
http cookie管理器(线程组>添加>配置元件>http cookie管理器)
察看结果树 (添加>监听器>察看结果树)
用表格察看结果 (添加>监听器>用表格查看结果)
汇总报告 (添加>监听器>汇总报告)
测试压测1
启动我们的nginx和项目以及redis连接工具,如下图所示:
数据库中的秒杀商品表中的商品出现了库存为负数的问题。
三、秒杀接口优化
1.优化第一步:解决超卖问题
更新秒杀商品库存的sql语句,只有当库存大于0才能更新库存;修改更新秒杀库存方法updateKillStockById的返回类型为boolean,用于判断是否更新成功。
OrderServiceImpl
@Transactional
@Override
public JsonResponseBody<?> createKillOrder(User user, Integer pid, Float price) {
//1.根据秒杀商品编号获取秒杀商品库存是否为空
//........
//2.秒杀商品库存减一
boolean flag = killService.update(new UpdateWrapper<Kill>()
.eq("item_id",pid)
.gt("total",0)
.setSql("total=total-1"));
if(!flag){
throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
}
//3.生成秒杀订单及订单项
//........
return new JsonResponseBody();
}
测试压测2
先把我们数据库回归到最初始的样子,执行以下SQL语句
TRUNCATE table zmall_order;
truncate table zmall_order_detail;
update zmall_kill set total = 10;
我们超卖的问题就解决了,有一些看似是没有问题的,但是在高并发的场景下面是存在问题的。
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);
}
IRedisService
public void setKillOrderToRedis(Integer pid, Order order);
public Order getKillOrderByUidAndPid(Integer uid, Integer pid);
这里用户抢购的秒杀订单保存到Redis默认设置是1800秒,即30分钟;可视情况具体调整。
OrderServiceImpl
@Autowired
private RedisServiceImpl redisService;
@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);
// 从redis中获取某个用户下的订单,如果存在订单,就意味着之前买过
//6.根据秒杀商品ID和用户ID判断是否重复抢购
Order order = redisService.getKillOrderByUidAndPid(user.getId(), pid);
if(null!=order)
throw new BusinessException(JsonResponseStatus.ORDER_REPART);
//2.秒杀商品库存减一
boolean flag = killService.update(new UpdateWrapper<Kill>()
.eq("item_id",pid)
.gt("total",0)
.setSql("total=total-1"));
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.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中
//生成秒杀订单等操作
//重点,重点,重点,在此处将生成的秒杀订单保存到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,完成秒杀商品预加载。
@Autowired
private IOrderService orderService;
@Autowired
private KillServiceImpl killService;
@Autowired
private RedisServiceImpl redisService;
@Override
public void afterPropertiesSet() throws Exception {
// 此方法仅在项目启动时加载 将秒杀商品的信息加载到redis中
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);
});
}
注意:可能会出错,原因是里面的Data是jdk8的,要换成jdk8之前的
② 预减库存
第一步:在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
@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);
// 通过reids获取商品的库存
//6.Redis库存预减
long stock = redisService.decrement(pid);
if(stock<0){
redisService.increment(pid);
throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
}
// 从redis中获取某个用户下的订单,如果存在订单,就意味着之前买过
//6.根据秒杀商品ID和用户ID判断是否重复抢购
Order order = redisService.getKillOrderByUidAndPid(user.getId(), pid);
if(null!=order)
throw new BusinessException(JsonResponseStatus.ORDER_REPART);
//2.秒杀商品库存减一
boolean flag = killService.update(new UpdateWrapper<Kill>()
.eq("item_id",pid)
.gt("total",0)
.setSql("total=total-1"));
if(!flag){
throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
}
//3.生成秒杀订单及订单项
//....
return new JsonResponseBody();
}
第三步:还原测试数据,重新使用Jmeter压测,这时可以发现明显压测效果要提升很多。
但是还是要根据不同电脑配置情况来决定,配置太低,效果也提升不了多少。
尤其是链接远程redis,会导致压测的吞吐量直线下降。
压测3
关于预减库存是先通过redis过滤一到,只要redis变为0了,它就不会操作数据库。
最后
以上就是激动宝贝为你收集整理的商品秒杀接口压测及优化一、生成测试用户二、jmeter压测三、秒杀接口优化 的全部内容,希望文章能够帮你解决商品秒杀接口压测及优化一、生成测试用户二、jmeter压测三、秒杀接口优化 所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复