Table of contents
1. Generate test users
Import the UserUtils tool class into the zmall-user module, run it to generate test user information, and generate the number of users according to your own computer conditions.
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\\xlb\\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(100);
}
}
1) It must be ensured that the zmall-user module is running and the test user data generation operation is being performed;
2) Pay attention to modify the user login interface address and port in UserUtils; at the same time, please modify the user login interface and store the generated token token in In the response package class;//5.通过UUID生成token令牌并保存到cookie中 String token= UUID.randomUUID().toString().replace("-",""); ... return new JsonResponseBody<>(token);
3) Set the storage location of the generated login token;
4) Modify the database name, login account and password;
5) Set the number of generated test users;
Two, jmeter pressure measurement
related configuration
1. Thread Schedule > Add > Thread (User) > Thread Group
2. Thread group>Add>Configuration element>http request default value
3. Thread Group > Add > Sampler > http request
4. Thread Group > Add > Configuration Elements > HTTP Cookie Manager
5. Thread Group>Add>Configuration Component>CSV Data File Settings
6. Thread Group > Add > Listener > Summary Report
7. Thread Group > Add > Listener > View Results Tree
8. Thread group > Add > Listener > View the results in a table
2.1 Test
There is a problem that the inventory of the products in the flash sale product table in the database is negative.
Lightning Deals are oversold in the order table and line item table
3. Optimization of the seckill interface
3.1 The first step of optimization: solving oversold
The sql statement for updating the stock of the seckill product can only be updated when the stock is greater than 0; modify the return type of update kill stock method updateKillStockById to boolean, which is used to determine whether the update is successful.
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);
//2.秒杀商品库存减一
boolean fiag = killService.update(new UpdateWrapper<Kill>()
.eq("item_id",pid)
.gt("total",0)
.setSql("total=total-1"));
if(!fiag)
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);
return new JsonResponseBody();
}
Test pressure test
3.2 The second step of optimization: Redis repeated buying
The following two methods are added in RedisService for the judgment operation of Redis repeated snap-up.
- According to the user ID and the seckill product ID as the Key, save the seckill order to Redis;
- Obtain the corresponding seckill product from Redis according to the user ID and seckill product ID;
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);
}
Here, the flash sale orders snapped up by users are saved to Redis. The default setting is 1800 seconds, that is, 30 minutes; it can be adjusted according to the situation.
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<>();
}
3.3 The third step of optimization: Redis pre-decrease inventory
3.3.1 Commodity initialization
Push the products that participate in the seckill activity, have the seckill status, and the seckill activity time is valid to Redis, and set the timeout period for the seckill products.
The timeout setting is the difference between the activity end time minus the activity start time, but it must be a valid activity time, that is, the current time is within the range of the activity start time and end time.
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
implements InitializingBean on the OrderController class in the zmall-order order module to complete the pre-loading of seckill products.
@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);
});
}
}
3.3.2 Advance inventory reduction
Step 1: Define inventory pre-decrease and increment methods in RedisService. The pre-reduction method is to pre-decrease the inventory of the product after the user successfully snaps up the product; the incremental method is that the Redis inventory pre-reduction may appear negative under high concurrency, and the inventory rollback is 0 through the incremental method
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);
}
Step 2: Modify the order generation method and add Redis inventory pre-decrease judgment
Please add the Redis inventory pre-decrease operation below the Redis repeated rush purchase judgment.
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);
//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);
//3.生成秒杀订单及订单项
//......
return new JsonResponseBody();
}
Step 3: Restore the test data and use jmeter pressure test again. At this time, it can be found that the pressure test efficiency has improved a lot.
However, it still depends on the configuration of different computers. If the configuration is too low, the efficiency will not be improved much.
Especially linking to remote redis will cause the throughput of pressure measurement to plummet