The pressure measurement and optimization of the commodity seckill interface of the seckill project

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

insert image description here

2. Thread group>Add>Configuration element>http request default value

insert image description here

3. Thread Group > Add > Sampler > http request

insert image description here

4. Thread Group > Add > Configuration Elements > HTTP Cookie Manager

insert image description here

5. Thread Group>Add>Configuration Component>CSV Data File Settings

insert image description here

6. Thread Group > Add > Listener > Summary Report

insert image description here

7. Thread Group > Add > Listener > View Results Tree
insert image description here

8. Thread group > Add > Listener > View the results in a table

insert image description here

2.1 Test

insert image description here
There is a problem that the inventory of the products in the flash sale product table in the database is negative.
insert image description here

Lightning Deals are oversold in the order table and line item table
insert image description here

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
insert image description here

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

insert image description here

Guess you like

Origin blog.csdn.net/qq_63531917/article/details/129015647