Redis combat - spike business optimization

Let's review the order process

When the user initiates a request, nginx will be requested at this time, nginx will access tomcat, and the program in tomcat will perform serial operations, divided into the following steps

1. Query coupons

2. Judging whether the seckill stock is sufficient

3. Query order

4. Check whether it is one person, one order

5. Deduction of inventory

6. Create an order

Previous spike business flow chart:

In these six steps, there are many operations to operate the database, and they are executed serially by one thread, which will cause our program to execute very slowly , so we need asynchronous program execution, so how to speed it up?

Optimization plan :

We put the time-consuming logical judgment into redis, such as whether the inventory is sufficient, such as whether one order per person, such an operation , as long as this logic can be completed, it means that we can definitely place an order and complete it . We only need to make quick logical judgments, and we don’t need to wait for the order logic to finish. We directly return success to the user , and then open a thread in the background . The background thread slowly executes the messages in the queue (queue) to complete the creation The action of the order (the asynchronous operation creates the order) . Isn't this program super fast? And there is no need to worry about the exhaustion of the thread pool, because here we do not use any thread pool manually in our program.

The specific implementation process:

First of all, in Reids to judge whether the conditions are met, we can use the method of list type in redis to store user information, key order key, value will be passed in the user id (multiple, so use the method of list type) , and in redis Enter inventory information .

Then write a Lua script to implement the business logic and determine whether the user is eligible to place an order.

The logic refers to the figure below.

 After writing the Lua script, you can execute the business logic for it in Java

1. First call the Lua script

2. Judgment result

3. If it is not 0, return an error

4. If it is 0, the user meets the conditions, the order information is passed into the blocking queue, and the thread is started to create the order asynchronously

5. Then order information

The specific flow chart is as follows:

Of course, there are two difficulties here,

The first difficulty is how do we quickly verify one person, one order in redis, as well as inventory judgment?

The second difficulty is how to put the order information into the blocking queue and create a new thread to create the order asynchronously?

For using Redis to quickly verify one person, one order, and inventory judgment , as mentioned above, it is to use Lua script

Specific Lua script code:

-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

Then put the order information into the blocking queue, and create a new thread to create the order asynchronously :

Reminder: This is just a pseudocode~ The specific code implementation is at the bottom

We can use the following code to create a blocking queue and create orders asynchronously

Create a blocking queue:

BlockingQueue<VoucherOrder> blockingQueue = new ArrayBlockingQueue<>(1024*1024)
//保存订单信息
blockingQueue.add(voucherOrder);
//获取订单信息
blockingQueue.tack();

To create an order asynchronously: 

    BlockingQueue<VoucherOrder> blockingQueue = new ArrayBlockingQueue<>(1024*1024)
    //获取线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

 //在类初始化的是否就执行异步下单的任务
    @PostConstruct
    public void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
    }

    //执行异步下单的任务
    private class VoucherOrderHandle implements Runnable{

        @Override
        public void run() {
            while (true){
                try {
                    //1.获取队列中的订单信息
                    VoucherOrder voucherOrder = blockingQueue.take();
                    //2.创建订单
                    handlerVoucherOrder(voucherOrder);
                } catch (InterruptedException e) {
                    log.error("获取订单信息异常{}",e);
                }
            }
        }
    }

Full code:

Lua script:

-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

Java code: 

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService iSeckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    //获取lua脚本
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }

    //获取阻塞队列
    private BlockingQueue<VoucherOrder> blockingQueue = new ArrayBlockingQueue<>(1024*1024);

    //获取线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    //在类初始化的是否就执行异步下单的任务
    @PostConstruct
    public void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
    }

    //执行异步下单的任务
    private class VoucherOrderHandle implements Runnable{

        @Override
        public void run() {
            while (true){
                try {
                    //1.获取队列中的订单信息
                    VoucherOrder voucherOrder = blockingQueue.take();
                    //2.创建订单
                    handlerVoucherOrder(voucherOrder);
                } catch (InterruptedException e) {
                    log.error("获取订单信息异常{}",e);
                }
            }
        }
    }

    private void handlerVoucherOrder(VoucherOrder voucherOrder) {
        //获取用户id
        Long userId = voucherOrder.getUserId();
        //1.创建锁对象
        //SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
        RLock lock = redissonClient.getLock("order:" + userId);

        //2.尝试获取锁
        boolean isLock = lock.tryLock();
        if (!isLock){
            //获取锁失败
            log.error("获取锁失败");
            return;
        }
        try {
             proxy.createVoucherOrder(voucherOrder);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    private IVoucherOrderService proxy;

    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();

        //1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        //2.判断返回结果是否为0
        int r = result.intValue();
        if (r != 0) {
            //3.如果不为0,代表没有下单资格
            Result.fail(r==1?"库存不足!":"不可重复下单!");
        }

        //4.如果为0,有购买资格,把下单信息保存到阻塞队列
        long order = redisIdWorker.nextId("order");
        //4.1 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //4.2添加订单id
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //4.3添加用户id
        voucherOrder.setUserId(userId);
        //4.3添加优惠券id
        voucherOrder.setVoucherId(voucherId);
        blockingQueue.add(voucherOrder);

        //5.获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        //6.返回一个订单id
        return Result.ok(order);
    }


    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        //6.根据优惠券id和用户id判断订单是否已经存在
        //如果存在,则返回错误信息
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        if (count > 0) {
            log.error("用户已经购买!");
            return;
        }
        boolean success = iSeckillVoucherService.update()
                .setSql("stock=stock-1")
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0)
                .update();
        if (!success){
            //扣减失败
            log.error("扣减失败");
            return;
        }
        save(voucherOrder);
    }
}

Guess you like

Origin blog.csdn.net/qq_59212867/article/details/128278257