Related requirements & instructions

Generally speaking, the second kill system will not have many functions, including:

1. Make a kill plan. What time will it start on a particular day, what goods will be sold, how many will be sold, and how long will it last.

2. Display the list of kill plans. They usually show the day, sell some at 8 o ‘clock, sell some at 10 o ‘clock.

3. Product details page.

4. Order and purchase.

And so on.

The main purpose of this article is to use code to implement functions to prevent overselling, so functions like making kill plans and displaying items won’t need to be rewritten.

In addition, e-commerce products are mainly spUS (for example, iPhone 12, iPhone 11 is two SPUs) and SKUS (for example, iPhone 12 64G white, iPhone 12 128G black is two SKUs). The display is SPUS, and the purchase of the inventory is SKUS. In this paper, for convenience, product is directly used instead.

There are also some pre-conditions, such as going through a risk control system to confirm that you are not a scalper; Marketing system, do you have relevant coupons, virtual currency and so on.

After placing an order, we need to go through warehouse management, logistics, and points, etc., which will not be involved in this paper.

This article does not involve the database, everything in Redis operation, but still want to talk about the database and cache data consistency problem.

If the concurrency of our system is not high and the database can hold, we can operate the database directly. In order to prevent oversold, we can adopt:

Pessimistic locking

select * fromSKU table where sku_id =1 for update;
Copy the code

Or optimistic locking

Update SKU set stock=stock-1 where sku_id=1And update_version= update_version;Copy the code

If the concurrency is high, for example, the commodity details page generally has the highest concurrency, in order to reduce the pressure of the database, Redis and other cache will be used, in order to ensure the consistency of the database and Redis, most of the “delete after modification” scheme is adopted.

However, in the case of higher concurrency in this scheme, such as C10K and C10M, a large number of queries will be transmitted to the database at the moment of modifying the database and deleting the Redis content, resulting in exceptions.

In this case, the SPU details interface must not be connected to the database.

The steps should be:

1. End B manages the system operation database (this concurrency is not high).

2. After the data is stored, a message is sent to MQ.

3. Upon receiving the Topic of the subscribed MQ, the relevant handler retrieves the information from the database and puts it into Redis.

4. Relevant service interfaces only fetch data from Redis.

Code implementation

In actual projects, it is recommended to combine the related interfaces of the second kill product at the ToC end into a micro service, product-server. The sales interface is combined into a microservice, order-server. You can refer to the previous Spring Cloud series for coding, which uses a simple Spring Boot project.

Second kill plan entity class:

Omit the get/set

public class SecKillPlanEntity implements Serializable {
    private static final long serialVersionUID = 8866797803960607461L;

    /** * id */
    private Long id;

    /** * product id */
    private Long productId;

    /** ** product name */
    private String productName;

    /** * Price unit: cent */
    private Long price;

    /** * line price unit: cent */
    private Long linePrice;

    /** ** inventory */
    private Long stock;

    /** * A user buys only one item
    private int buyOneFlag;

    /** * Plan status 0 not committed, 1 committed */
    private int planStatus;

    /** * start time */
    private Date startTime;

    /** * End time */
    private Date endTime;

    /** * create time */
    private Date createTime;
}
Copy the code

Description:

1. As mentioned above, SPU should be displayed for the goods in seconds killing, and SKU is sold for the stock. For convenience, only Product is used in this paper.

2. There are two ways for users to purchase seckill products:

A. One user is only allowed to purchase one item.

B. One user can buy more than one item multiple times.

So this class uses buyOneFlag as its identifier.

3. PlanStatus indicates whether the second kill is actually executed. 0 is not displayed to terminal C and is not sold; 1 show it to terminal C for sale.

Add SEC kill plan & Query SEC kill plan:

@RestController
public class ProductController {

    @Resource
    private RedisTemplate<String.String> redisTemplate;

    // Generate a random kill plan to Redis
    @GetMapping("/addSecKillPlan")
    @ResponseBody
    public DefaultResult<List<SecKillPlanEntity>> addSecKillPlan(@RequestParam("saledate") String saleDate) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        Random rand = new Random();
        Gson gson = new Gson();
        List<SecKillPlanEntity> list = Lists.newArrayList();

        for (int i = 0; i < 10; i++) {
            long productId = rand.nextInt(100) + 1;
            long price = rand.nextInt(100) + 1;
            long stock = rand.nextInt(100) + 1;

            String saleStartTime = "10:00:00";
            String saleEndTime = "12:00:00";
            int buyOneFlag = 0;
            if (i > 4) {
                saleStartTime = "14:00:00";
                saleEndTime = "16:00:00";
                buyOneFlag = 1;
            }

            SecKillPlanEntity entity = new SecKillPlanEntity();
            entity.setId(i + 1L);
            entity.setProductId(productId);
            entity.setProductName("Goods" + productId);
            entity.setBuyOneFlag(buyOneFlag);
            entity.setLinePrice(999999L);
            entity.setPlanStatus(1);
            entity.setPrice(price * 100);
            entity.setStock(stock);
            entity.setEndTime(Date
                    .from(LocalDateTime.parse(saleDate + saleEndTime, dtf).atZone(ZoneId.systemDefault()).toInstant()));
            entity.setStartTime(Date.from(
                    LocalDateTime.parse(saleDate + saleStartTime, dtf).atZone(ZoneId.systemDefault()).toInstant()));
            entity.setCreateTime(new Date());

            // Write item details to Redis
            ValueOperations<String.String> setProduct = redisTemplate.opsForValue();
            setProduct.set("product_" + productId, gson.toJson(entity));
            // Write inventory
            if (buyOneFlag == 1) {
                // A user buys only one item
                // item purchase user Set
                redisTemplate.opsForSet().add("product_buyers_" + productId, "");
                // Inventory of goods
                for (int j = 0; j < stock; j++) {
                    redisTemplate.opsForList().leftPush("product_one_stock_" + productId, "1"); }}else {
                // Users can buy multiple
                redisTemplate.opsForValue().set("product_stock_" + productId, stock + "");
            }
            list.add(entity);
            System.out.println(gson.toJson(entity));
        }
        redisTemplate.opsForValue().set("seckill_plan_" + saleDate, gson.toJson(list));

        return DefaultResult.success(list);
    }

    @GetMapping("/findSecKillPlanByDate")
    @ResponseBody
    public DefaultResult<List<SecKillPlanEntity>> findSecKillPlanByDate(@RequestParam("saledate") String saleDate) {
        Gson gson = new Gson();
        String planJson = redisTemplate.opsForValue().get("seckill_plan_" + saleDate);
        List<SecKillPlanEntity> list = gson.fromJson(planJson, new TypeToken<List<SecKillPlanEntity>>() {
        }.getType());
        // Set up a new inventory
        for (SecKillPlanEntity entity : list) {
            if (entity.getBuyOneFlag() == 1) {
                long newStock = redisTemplate.opsForList().size("product_one_stock_" + entity.getProductId());
                entity.setStock(newStock);
            } else {
                long newStock = Long
                        .parseLong(redisTemplate.opsForValue().get("product_stock_"+ entity.getProductId())); entity.setStock(newStock); }}returnDefaultResult.success(list); }}Copy the code

Description:

1. AddSecKillPlan is to randomly generate 10 sales plans, including those for only one item and those for multiple items. And push the relevant data into Redis.

Seckill_plan_ Date, which represents all seckill plans on a certain day.

Product_ commodity ID, representing the information of a commodity, used in the details page.

Product_one_stock_ product ID, represents the number of inventory for only one item, the value is List, how many inventory, push “1” to it.

Product_buyers_ product ID, which represents the buyer of only one item. Users who have already bought one item are not allowed to buy another.

Product_stock_ Item ID, which represents the number of items in stock available for sale. The value is the number of items in stock.

2, findSecKillPlanByDate, display a certain day second kill sales plan. The inventory number is taken from the two keys associated with the inventory.

The LUA script:

Only one buyone.lua:

-- product_one_stock_XXX local stockKey = KEYS[1] -- product_buyers_XXX local buyersKey = KEYS[2] select * from user where user ID = 13Local result=redis.call()"sadd" , buyersKey , uid )
if(tonumber(result)==1Local stock=redis.call() local stock=redis.call("lpop", stockKey) -- except nil andfalse, all other values are true (including0)if(Stock) then -- to have inventoryreturn 1
    else-- No stockreturn -1
    end
else-- Yes, I havereturn -3
end
Copy the code

Can sell more than one buymore. Lua:

Local Key = KEYS[1Local val = ARGV[1Local stock = redis.call()"GET", key)
if (tonumber(stock)<=0) Then -- No inventoryreturn -1
elseLocal decrstock=redis.call()"DECRBY", key, val)
    if(tonumber(decrstock)>=0) Then -- No oversold after deducting the number of purchases, return to current stockreturn decrstock
    else-- Oversold, add the deduction back"INCRBY", key, val)
        return -2
    end
end
Copy the code

Description:

1. Only one item. Sadd = product_buyers_ product ID; if 1 is returned, it indicates that the user has not purchased the product before; otherwise -3 is returned, it indicates that the user has purchased the product.

Lpop value from product_one_stock_ item ID, return 1 if there is inventory, nil otherwise, no inventory.

2, can sell many pieces. I talked about it before. I’m not going to describe it.

Place the two Lua files in the Resources directory of the Spring Boot project.

Sales Interface:

@RestController
public class OrderController {

    @Resource
    private RedisTemplate<String.String> redisTemplate;

    @GetMapping("/addOrder")
    @ResponseBody
    public DefaultResult<Void> addOrder(@RequestParam("uid") long userId, @RequestParam("pid") long productId,
            @RequestParam("quantity") int quantity) {
        Gson gson = new Gson();
        String productJson = redisTemplate.opsForValue().get("product_" + productId);
        SecKillPlanEntity entity = gson.fromJson(productJson, SecKillPlanEntity.class);
        //TODO verifies that the sale plan has been submitted and that it is time to sell
        long code = 0;
        if (entity.getBuyOneFlag() == 1) {
            // The user only buys one item
            code = this.buyOne("product_one_stock_" + productId, "product_buyers_" + productId, userId);
        } else {
            // The user buys multiple items
            code = this.buyMore("product_stock_" + productId, quantity);
        }
        DefaultResult<Void> result = DefaultResult.success(null);
        // Error code handling should use ENUM, this article saves
        if (code < 0) {
            result.setCode(code);
            if (code == -1) {
                result.setMsg("No inventory");
            } else if (code == -2) {
                result.setMsg("Out of stock");
            } else if (code == -3) {
                result.setMsg("Purchased already"); }}return result;
    }

    private Long buyOne(String stockKey, String buysKey, long userId) {
        DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>();
        defaultRedisScript.setResultType(Long.class);
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("buyone.lua")));
        // "{pre}:"
        List<String> keys = Lists.newArrayList(stockKey, buysKey, userId + "");

        Long result = redisTemplate.execute(defaultRedisScript, keys, "");

        return result;
    }

    private Long buyMore(String stockKey, int quantity) {
        DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>();
        defaultRedisScript.setResultType(Long.class);
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("buymore.lua")));
        List<String> keys = Lists.newArrayList(stockKey);
        Long result = redisTemplate.execute(defaultRedisScript, keys, quantity+"");
        returnresult; }}Copy the code

Description:

BuyOne, buyMore, buyOne, buyMore, buyOne, buyMore, buyOne, buyOne, buyMore

In addition, I read that if the Redis cluster is used, an error will be reported, because I do not have the Redis cluster environment, so I can not test, you can have a try.

2, addOrder has some code to save time, it is written very low, for example, some verification is not added, error code should use ENUM, etc.

Test cases:

1. User A buys only one item 1, successful.

2. User A buys only one item 1 again and fails.

3, N Users buy only one product 1, the inventory is insufficient.

4. A Users can buy more than one product. 2.

5. User A can sell more than one product when purchasing. 2.

I have run under the environment of this machine, no problem, the result I will not put.


The first article of the year of the Ox, if you feel good, want to continue to see, but also please pay attention to, like, comment, collect, forward, pay attention to the public number: Kylin bug, share more technology, learn Java together, break technical difficulties!