主要使用的技术
1.SpringCloud
2.RocketMq
数据库建表
主要的表有 商品表、商品流水表、订单表
(大致的关系,主要是为了方便模拟,所以简化了表之间关系与字段)
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`goods_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商品唯一标识',
`goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名',
`num` bigint(11) NULL DEFAULT NULL COMMENT '商品库存',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`goods_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for goods_flow
-- ----------------------------
DROP TABLE IF EXISTS `goods_flow`;
CREATE TABLE `goods_flow` (
`flow_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商品流水唯一流水号标识',
`goods_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品唯一标识',
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作类别',
`user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户唯一标识',
`order_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单唯一标识',
`num` bigint(11) NULL DEFAULT NULL COMMENT '数量',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`is_delete` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否删除',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`flow_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`order_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单唯一标识',
`user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户唯一标识',
`order_status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单状态(订单创建>订单支付>订单生产>订单确认>订单完成)',
`goods_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品唯一标识',
`goods_num` int(11) NULL DEFAULT NULL COMMENT '商品数量',
`count_fee` decimal(10, 0) NULL DEFAULT NULL COMMENT '总计费用',
`step` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '步骤',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
过程逻辑
其中分解为两个微服务,分别为订单微服务和商品微服务
下单流程
orderController
/**
* 下单
* @return
*/
@PostMapping("/submit")
public R<?> submit(String goodsId,Long num){
Order order = new Order();
order.setUserId("user1");
order.setCreateTime(LocalDateTime.now());
order.setGoodsId(goodsId);
order.setGoodsNum(num.intValue());
order.setOrderStatus("订单创建");
//支付状态
order.setStep("N");
//提交订单 未支付
boolean submitIsSuccess = iOrderService.submit("user1", order);
return R.ok(submitIsSuccess);
}
orderService
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submit(String userId, Order order) {
//存储订单 saveOrder
this.baseMapper.insert(order);
//调用商品服务 锁定库存
Long goodsNum = order.getGoodsNum().longValue();
R<?> lock = remoteGoodsService.lock(order.getGoodsId(), goodsNum, order.getOrderId(), order.getUserId());
// 锁定不成,抛异常,让回滚订单
if (lock.getCode()!= 200) {
throw new RuntimeException(lock.getMsg());
}
//发送消息,如果三十分钟后没有支付,则取消订单
SendStatus sendStatus = orderCancelTemplate
.syncSend(RocketMqConstant.ORDER_CANCEL_TOPIC,
new GenericMessage<>(order.getOrderId()),
RocketMqConstant.TIMEOUT,
RocketMqConstant.CANCEL_ORDER_DELAY_LEVEL).getSendStatus();
if (!Objects.equals(sendStatus,SendStatus.SEND_OK)) {
// 消息发不出去就抛异常,发的出去无所谓
throw new RuntimeException("系统异常");
}
return true;
}
取消订单流程
(1)用户主动取消订单
orderController
/**
* 取消订单
*/
@PutMapping("/cancel/{orderId}")
public R<String> cancelOrder(@PathVariable("orderId") String orderId) {
String userId = "user1";
LambdaQueryWrapper<Order> wrapper = new QueryWrapper<Order>().lambda()
.eq(Order::getOrderId, orderId)
.eq(Order::getUserId, userId);
Order order = iOrderService.getOne(wrapper);
if (order.getOrderStatus() == "订单支付") {
// 订单已支付,无法取消订单
return R.fail("订单已支付,不可取消");
}
// 如果订单未支付的话,将订单设为取消状态
iOrderService.cancelOrder(order.getOrderId());
return R.ok();
}
orderService
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(String orderId) {
log.info("cancelOrder:{}",orderId);
LambdaQueryWrapper<Order> queryWrapper = new QueryWrapper<Order>()
.lambda().eq(Order::getOrderId, orderId).eq(Order::getStep, "N");
Order order = this.baseMapper.selectOne(queryWrapper);
if (order.getOrderStatus() == "取消订单") {
return;
}
//订单支付状态为N,设置订单状态为 取消订单
this.baseMapper.cancelOrder("取消订单",order.getOrderId());
// 通知-解锁库存状态
SendStatus stockSendStatus = stockMqTemplate
.syncSend(RocketMqConstant.STOCK_UNLOCK_TOPIC, new GenericMessage<>(orderId)).getSendStatus();
if (!Objects.equals(stockSendStatus,SendStatus.SEND_OK)) {
// 消息发不出去就抛异常,发的出去无所谓
throw new RuntimeException("系统异常");
}
}
商品库存锁定和取消锁定流程
goodsService
@Service
@Slf4j
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {
@Autowired
private RocketMQTemplate stockMqTemplate;
@Resource
private GoodsFlowMapper goodsFlowMapper;
@Resource
private RemoteOrderService remoteOrderService;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean lock(String goodsId, Long num,String orderId,String userId){
// 减商品库存
int reduceStockIsSuccess = this.baseMapper.reduceStock(goodsId, num);
if(reduceStockIsSuccess<1){
throw new RuntimeException("库存不足!goodsId:"+goodsId);
}
// (记入商品库存流水日志)保存库存锁定信息
GoodsFlow goodsFlow = new GoodsFlow();
goodsFlow.setUserId(userId);
goodsFlow.setNum(num);
goodsFlow.setGoodsId(goodsId);
goodsFlow.setOrderId(orderId);
goodsFlow.setType("-");
goodsFlow.setIsDelete("N");
goodsFlow.setCreateTime(LocalDateTime.now());
goodsFlowMapper.insert(goodsFlow);
// 一个小时后解锁库存
//延时信息无法立即在可视化界面上查看
SendStatus sendStatus =
stockMqTemplate.syncSend(RocketMqConstant.STOCK_UNLOCK_TOPIC, new GenericMessage<>(orderId)
, RocketMqConstant.TIMEOUT, RocketMqConstant.CANCEL_ORDER_DELAY_LEVEL + 1).getSendStatus();
//stockMqTemplate.convertAndSend(RocketMqConstant.STOCK_UNLOCK_TOPIC, "order-string-test");
if (!Objects.equals(sendStatus,SendStatus.SEND_OK)) {
// 消息发不出去就抛异常,发的出去无所谓
throw new RuntimeException("服务器出了点小差");
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void unlockStock(String orderId) {
log.info("unlockStock:{}",orderId);
//远程调用 获取订单状态
R<Order> orderResponse = remoteOrderService.getById(orderId);
log.info("unlockStock.orderResponse:{}",orderResponse.toString());
if (orderResponse.getCode()!=200) {
throw new RuntimeException(orderResponse.getMsg());
}
Order order = orderResponse.getData();
// 该订单没有下单成功,或订单已取消,赶紧解锁库存
if(order.getOrderStatus().equals("取消订单") == false){
return;
}
//根据订单号 获取锁定的库存
LambdaQueryWrapper<GoodsFlow> queryWrapper = new QueryWrapper<GoodsFlow>()
.lambda().eq(GoodsFlow::getOrderId, orderId);
GoodsFlow goodsFlow = goodsFlowMapper.selectOne(queryWrapper);
goodsFlow.setIsDelete("Y");
//还原商品库存
this.baseMapper.addStock(goodsFlow.getGoodsId(),goodsFlow.getNum());
//解锁库存
goodsFlowMapper.updateById(goodsFlow);
}
}
监听消息队列中的订单过期为未支付消息
@Component
@RocketMQMessageListener(topic = RocketMqConstant.ORDER_CANCEL_TOPIC,consumerGroup = RocketMqConstant.ORDER_CANCEL_TOPIC)
public class OrderCancelConsumer implements RocketMQListener<String> {
@Autowired
private IOrderService orderService;
/**
* 订单取消状态修改后再进行其他服务
*/
@Override
public void onMessage(String orderId) {
// 如果订单未支付的话,将订单设为取消状态
orderService.cancelOrder(orderId);
}
}
监听消息队列中的取消锁定操作消息
@Component
@RocketMQMessageListener(topic = RocketMqConstant.STOCK_UNLOCK_TOPIC,consumerGroup = RocketMqConstant.STOCK_UNLOCK_TOPIC)
public class StockUnlockConsumer implements RocketMQListener<String> {
@Autowired
private IGoodsService goodsService;
/**
* 1、库存锁定一定时间后,如果订单支付未支付,则解锁库存(有可能库存锁定成功,订单因为异常回滚导致订单未创建)
* 2、取消订单,直接解锁库存
*/
@Override
public void onMessage(String orderId) {
goodsService.unlockStock(orderId);
}
}
测试
商品库存500,线程数开启1000并发值,测试下单数是否符合库存数,并测试订单过期库存回滚操作。