Introduction to the

This is a simple commodity kill server, through Redis to achieve fast read and write inventory, to achieve a commodity granularity of distributed lock to prevent concurrent operation of inventory problems. The successful order is sent as a message to MQ, which can then be consumed in the goods service to create an order drop-off library, the consumer part of which is not implemented in this project.

Kill the process

  1. The blocker puts a portion of the requests in every second, and the filtered request returns a snap failure.
  2. Make basic checks on sales volume, events, whether items are sold out,
  3. Try to deduct stock:
    1. Try to request a lock on the item,
    2. Get the inventory of the item, if the inventory is 0, add to the sold out queue, return failed to buy,
    3. Minus inventory,
    4. Generate order to send MQ,
    5. Purchase success, return to order number.

The main logic

Use distributed locks to lock the same snap – up item, thereby reducing inventory

  1. Because there can be multiple item description activities, you need to get the lock by item. Define a custom annotation to indicate the lock:
@target (ElementType.METHOD) @Retention(RetentionPolicy.runtime) @Documented Public @interface OrderLock {// Product code String  value() default ""; }Copy the code
  1. The subtracting inventory method annotates OrderLock and sets the value of the locked object via the EL expression:
@Override
@OrderLock("#activity.itemCode")
public long stockReduce(String userId, int quantity, SeckillActivityInfo activity) {
    ...
}
Copy the code
  1. Try to lock the inventory of the commodities snapped up through the surround section, and release the lock after successful processing:
@Around("@annotation(orderLock)") public Object invoke(ProceedingJoinPoint pjp, OrderLock orderLock) throws Throwable { Object lockObject = getLockObject(pjp, orderLock.value()); If (lockObject == null) {throw new LockFailException(" can't get lockObject "); } RLock lock = redissonClient.getLock(lockObject.toString()); boolean isLock = false; Test {isLock = lock.tryLock(1000, 5000, timeunit.milliseconds); if (isLock) { return pjp.proceed(); } throw new LockFailException(" object "+ lockObject + "" LockFailException "); } finally { if (isLock) { lock.unlock(); }}}Copy the code

Use RLocalCachedMap to cache activity information data

In fact, before each purchase request is processed, it is necessary to query the purchase activity information to obtain the activity commodity information, activity price and other data. This is a highly frequent read operation, and the active information data is not updated during this time, so there is no need to query remote data and network traffic will be a bottleneck. Redisson provides a distributed cache map with local caching capabilities. RLocalCachedMap is just right for this scenario. RLocalCachedMap inherited from the RMap, implements the Java. Util. Concurrent. The ConcurrentMap and Java. Util. The two interfaces, the local cache function fully exploited the JVM itself memory space, a local cache of some commonly used elements, Read operations are officially up to 45 times better than distributed mapping. What are you waiting for? Use it.

Specific steps for inventory reduction

  1. To acquire inventory of items that are being snapped up,
RBucket<ItemStockBase> bucket = redissonClient.getBucket(key);
ItemStockBase stock = bucket.get();
Copy the code
  1. Determine whether the inventory is equal to 0, if the inventory is equal to 0, add the item to the sold out set, return failed to buy,
If (stockQuantity == 0) {RSet<String> itemSaleOut = redissonClient.getSet(ITEM_SALE_OUT); itemSaleOut.add(itemCode); return -1; }Copy the code
  1. Whether the buying quantity is greater than the inventory, the inventory is insufficient, the buying failure,
  2. Update inventory,
stock.setQuantity(stockQuantity - quantity);
bucket.set(stock);
Copy the code
  1. Generate order to send MQ,
SeckillOrder order = createSeckillOrder(userId, activity, quantity); SendResult sr = orderProducerBean.send(order, seckillOrderTopic); if (sr.isSuccess()) { return order.getOrderNo(); } else { return -1; }Copy the code

It should be noted that sending MQ may fail, so we still need to judge whether the purchase is successful according to the result of SEND. This step should be after deducting redis inventory to ensure that it will not be oversold, but it may be undersold.

test

You can test the second kill interface through Jmeter concurrently and participate in the buying up of two products at the same time.

  • /seckill/init Initializes the seckill activity information. Preset activity information, commodity inventory information. It is recommended to delete redis data before each pressure test and re-call this interface to initialize the data.
  • /seckill/order Indicates the seckill interface.

conclusion

Seckill system is very complex, here only simulated the workflow of the server, and did not involve the front-end, load balancing and other parts. The idea of high concurrency is to avoid frequent read-write database IO operations, use MQ to generate orders asynchronously, and ensure that they are not oversold.

  • Generate distributed locks from the commodity dimension, which can support buying activities of multiple different commodities.
  • Request filtering (traffic limiting on interfaces). Panic buying is originally a probability event, 1000 goods, 1 million people snap up, 1 in 1000 success rate, can not check the 1 million requests to redis inventory, so Redis can not carry, so random discard part of the request is clever and reasonable. This is to configure a limiting interceptor for the SEC kill interface to filter requests,
  • Prevent oversold. As for destocking and sending MQ, it should be destocking first and then sending MQ, because sending MQ may fail. The order in MQ must have been deducted from the inventory and will not be oversold. Some people say that there is no need to deduct the redis stock according to the result of MQ, or send MQ failure and then add back the stock, it is better to buy less than to sell more. In fact, MQ is very reliable and I have tested many million concurrent requests without underbuying.

The complete source code has been uploaded: github.com/Phantom0103… Please give more advice to help me improve, 🙏!