在高并发扣减库存中,我们可以通过合并请求的方式来进行批次的扣减库存,这样子可以降低数据库的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();
}
});
}