合并扣减库存Demo

276 阅读2分钟

在高并发扣减库存中,我们可以通过合并请求的方式来进行批次的扣减库存,这样子可以降低数据库的io,其实主要优点就是降低了数据库的io,当然在高并发扣减库存中有很多方案例如通过redis扣减库存,通过数据库的乐观锁悲观锁扣减库存,而合并请求只是一种可行方法,具体问题需要具体对待。

下面是示例代码:

package com; 
import com.sun.org.apache.xpath.internal.functions.FuncTrue;
import com.sun.xml.internal.ws.api.ha.StickyFeature;
import lombok.*;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class KillDemo {
    /**
     * 启动10个线程
     * 库存6个
     * 生成一个合并队列
     * 每个用户拿到自己的请求响应
     */
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        KillDemo killDemo = new KillDemo();
        killDemo.mergeJob();
        List<Future<Result>> futureList = new ArrayList<>();
        //用来模拟并发
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            final Long orderId = i+ 100l;
            final Long userId = Long.valueOf(i);
            Future<Result> future = executorService.submit(() -> {
                countDownLatch.countDown();
                countDownLatch.await();
                return killDemo.operate(new UserRequest(orderId, userId, 1));
            });
            futureList.add(future);
        }

        futureList.forEach(future -> {
            try {
                Result result = future.get(300, TimeUnit.MILLISECONDS);
                System.out.println(Thread.currentThread().getName() + "客户端请求响应" +result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        });
    }

    private Integer stock = 6;

    //创建一个阻塞队列
    private BlockingQueue<RequestPromise> queue = new LinkedBlockingQueue<>(10);



    public Result operate(UserRequest userRequest) throws InterruptedException {
        RequestPromise requestPromise = new RequestPromise(userRequest);

        //这个同步代码块不是为了加锁只是为了 使用wait() 方法
        synchronized (requestPromise) {
            //确保先入队列 等待 再唤醒
            boolean enqueueSuccess = queue.offer(requestPromise, 100, TimeUnit.MILLISECONDS);
            if (!enqueueSuccess) {
                return new Result(false, "系统繁忙");
            }
            try {
                requestPromise.wait(200);
                if(requestPromise.getUserRequest() == null) {
                    return new Result(false, "等待超时");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return requestPromise.getResult();
    }

    public void mergeJob() {
        new Thread(
                () -> {
                    List<RequestPromise> list = new ArrayList<>();
                    while (true) {
                        if (queue.isEmpty()){
                            try {
                                //避免cpu空轮询
                                Thread.sleep(10);
                                continue;
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        int batchSize = queue.size();
                        for (int i = 0; i < batchSize; i++) {
                            list.add(queue.poll());

                        }

                        System.out.println(Thread.currentThread().getName() + ":合并扣减库存:" + list);
                        int sum = list.stream().mapToInt(e ->e.getUserRequest().getCount()).sum();
                        if (sum <= stock) {
                            stock -= sum;
                            //遍历唤醒之前阻塞的线程
                            list.forEach(requestPromise -> {
                                requestPromise.setResult(new Result(true, "ok"));
                                synchronized (requestPromise) {
                                    requestPromise.notify();
                                }
                            });
                            continue;
                        }
                        //遍历循环每一次的扣减数量判断库存是否足够
                        for (RequestPromise requestPromise : list) {
                            requestPromise.setResult(new Result(true, "ok"));
                            int count = requestPromise.getUserRequest().getCount();
                            if(count <= stock) {
                                stock -= count;
                            }else {
                                requestPromise.setResult(new Result(false, "库存不足"));
                            }
                            synchronized (requestPromise) {
                                requestPromise.notify();
                            }
                        }
                        list.clear();
                    }
                }
        , "mergeThread").start();
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
class RequestPromise {
    private Result result;
    private UserRequest userRequest;

    public RequestPromise(UserRequest userRequest) {
        this.userRequest = userRequest;
    }
}

@Data
@AllArgsConstructor
@ToString
class Result {
    private boolean success;
    private String msg;
}

@Data
@ToString
@AllArgsConstructor
class UserRequest {
    private Long orderId;
    private Long userId;
    private Integer count;
}

其实核心主要是通过 RequestPromise 对象实现对请求和返回值的绑定,通过 RequestPromise 对象锁实现 wait()和notify()

如有问题请指正

----------------------------------------分割线-----------------------------------------------

思考上面demo代码 因为扣减库存是启动异步线程去执行的,如果客户端请求超时了,但是依然扣减库存了怎么办?

通过新增流水日志表来对结果进行回滚

//模拟日志操作表
    private List<OperateChangeLog> operateChangeLogs = new ArrayList<>();
    //模拟添加数据库
    private void saveChangeLog(List<UserRequest> list ,int operateType) {
        List<OperateChangeLog> collect = list.stream().map(userRequest ->
                new OperateChangeLog(userRequest.getOrderId(), userRequest.getCount(), operateType))
                .collect(Collectors.toList());
        operateChangeLogs.addAll(collect);
    }

    private void rollBack(UserRequest userRequest) {
        if(operateChangeLogs.stream()
                .anyMatch(operateChangeLog -> operateChangeLog.getOrderId()
                        .equals(userRequest.getOrderId()))) {
            //如果扣减库存流水表里面有该订单id 进行回滚
            //判断是否已经回滚过
            boolean hasRollBack = operateChangeLogs.stream().anyMatch(operateChangeLog -> {
                return operateChangeLog.getOrderId().equals(userRequest.getOrderId()) && operateChangeLog.getOperateType().equals(2);
            });
            if(hasRollBack) return;
            //加回库存
            stock += userRequest.getCount();
            List list = new ArrayList<UserRequest>();
            list.add(userRequest);
            saveChangeLog(new ArrayList<UserRequest>(list), 2);
        }
        // 忽略
    }
    public void mergeJob() {
        new Thread(
                () -> {
                    List<RequestPromise> list = new ArrayList<>();
                    while (true) {
                        if (queue.isEmpty()){
                            try {
                                //避免cpu空轮询
                                Thread.sleep(10);
                                continue;
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        //改为取固定长度的请求处理
                        for (int i = 0; i < 3; i++) {
                            try {
                                list.add(queue.take());
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }

                        System.out.println(Thread.currentThread().getName() + ":合并扣减库存:" + list);
                        int sum = list.stream().mapToInt(e ->e.getUserRequest().getCount()).sum();
                        if (sum <= stock) {
                            // 开始事务!!!!!!重点 扣减库存和记录库存扣减流水必须是原子性的在一个是事务里
                            stock -= sum;
                            //记录扣减库存日志 方便后来进行回滚
                            saveChangeLog(list.stream().map(RequestPromise::getUserRequest).collect(Collectors.toList()), 1);
                            //提交事务
                            //遍历唤醒之前阻塞的线程
                            list.forEach(requestPromise -> {
                                requestPromise.setResult(new Result(true, "ok"));
                                synchronized (requestPromise) {
                                    requestPromise.notify();
                                }
                            });
                            continue;
                        }
                        //遍历循环每一次的扣减数量判断库存是否足够
                        for (RequestPromise requestPromise : list) {
                            requestPromise.setResult(new Result(true, "ok"));
                            int count = requestPromise.getUserRequest().getCount();
                            if(count <= stock) {
                                // 开始事务!!!!!!重点 扣减库存和记录库存扣减流水必须是原子性的在一个是事务里
                                stock -= count;
                                saveChangeLog(list.stream().map(RequestPromise::getUserRequest).collect(Collectors.toList()), 1);
                                //提交事务
                            }else {
                                requestPromise.setResult(new Result(false, "库存不足"));
                            }
                            synchronized (requestPromise) {
                                requestPromise.notify();
                            }
                        }
                        list.clear();
                    }
                }
        , "mergeThread").start();
    }
}

 
@Data
@AllArgsConstructor
@NoArgsConstructor
class OperateChangeLog {
    private  Long orderId;
    private Integer count;
    // 1 扣减 2 回滚
    private Integer operateType;
} 
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    KillDemo killDemo = new KillDemo();
    killDemo.mergeJob();
    List<Future<Result>> futureList = new ArrayList<>();
    //用来模拟并发
    CountDownLatch countDownLatch = new CountDownLatch(10);
    Map<UserRequest, Future<Result>> requestFutureMap = new HashMap<>();
    for (int i = 0; i < 10; i++) {
        final Long orderId = i+ 100l;
        final Long userId = Long.valueOf(i);
        UserRequest userRequest = new UserRequest(orderId, userId, 1)
        Future<Result> future = executorService.submit(() -> {
            countDownLatch.countDown();
            countDownLatch.await();
            return killDemo.operate(userRequest);
        });
        requestFutureMap.put(userRequest, future);
    }

    requestFutureMap.entrySet().forEach(entry -> {
        try {
            Result result = entry.getValue().get(300, TimeUnit.MILLISECONDS);
            System.out.println(Thread.currentThread().getName() + "客户端请求响应" +result);
            //如果相应失败进行回滚
            if(!result.isSuccess()) {
                //超时进行请求回滚
                killDemo.rollBack(entry.getKey());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    });
}