幂等性: 多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
目录
1.4.5PROPAGATION_NOT_SUPPORTED
一、天然的幂等行为
1.以SQL语句为例:
select * from t_user where id = 1;
update t_user set age = 18 where id = 2;
delete from t_user where id = 1
insert into (userid,username)values(1,'波哥') ; # userid 唯一主键
不具备幂等行为的
update t_user set age = age + 1 where id = 1;
insert into (userid,username)values(1,'波哥'); # userid 不是主键 可以重复
2.需要幂等行为的场景
需要使用幂等的场景 :
-
前端重复提交
-
接口超时重试
-
消息队列重复消费
3.解决方案
token机制 :①客户端请求获取token,服务端生成一个唯一ID作为token存在redis中;②客户端第二次请求时携带token,服务端校验token成功则执行业务操作并删除token,服务端校验token失败则表示重复操作。
基于mysql :①新建去重表;②服务端将客户端请求时提交的部分信息放入表中,其中有唯一索引字段;③成功插入则没有重复请求,插入失败则重复请求。
基于redis :①客户端请求服务端拿本次请求的标识字段;②服务端将标识字段以setnx方式存入redis并设置过期时间;③设置成功则说明非重复操作,设置失败则表示重复操作。
状态机、悲观锁、乐观锁等。
二、订单服务应用
1.订单提交流程
订单生成相关信息梳理
2.生成防重Token
我们在获取订单结算页数据的service中我们需要生成对应的Token,并且保存到Redis中同时绑定到页面。
/**
* 查询订单确认信息
* @return
*/
@Override
public OrderConfirmVo toConfirm() {
MemberDTO memberInfo = getMemberInfo();
Long memberId = memberInfo.getId();
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
//获取RequestContextHolder.getRequestAttributes() RequestContextHolder.getRequestAttributes()
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//同步设置请求头
RequestContextHolder.setRequestAttributes(requestAttributes);
//1.会员地址信息
List<MemberAddressVo> address = memberFeignService.getAddress(memberId);
orderConfirmVo.setAddress(address);
}, threadPoolExecutor);
CompletableFuture<Void> cartItemsFuture = CompletableFuture.runAsync(() -> {
//同步设置请求头
RequestContextHolder.setRequestAttributes(requestAttributes);
//2.购物车选中的商品信息
List<OrderItemVo> cartItems = cartFeignService.getCartItems(memberId);
orderConfirmVo.setItems(cartItems);
}, threadPoolExecutor);
//3.积分信息
//4.优惠券信息
//5.发票信息
//6.其他信息
//7.计算价格--计算订单的总金额 订单支付的总金额
//8.返回确认信息
CompletableFuture.allOf(addressFuture, cartItemsFuture).join();
System.out.println("当前用户"+memberInfo+"订单确认信息为"+orderConfirmVo.toString());
//9.防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
orderConfirmVo.setOrderToken(token);
//10.缓存防重令牌
redisTemplate.opsForValue().set(OrderConstant.ORDER_TOKEN_PREFIX+memberId,token);
return orderConfirmVo;
}
3.页面中的处理
4.提交订单Vo构建
MemberAddressVoOrderConfirmVoOrderItemSpuInfoVoOrderItemVoOrderResponseVoOrderSubmitVo
package com.yueluo.mall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 订单提交vo
* @author xjx
* @email 15340655443@163.com
* @date 2024/2/15 16:27
*/
@Data
public class OrderSubmitVo {
// 订单的收货地址id
private Long addrId;
// 支付方式
private Integer payType;
//支付总金额
private BigDecimal payTotal;
//订单总金额
public BigDecimal total;
// 防重令牌
private String orderToken;
// 订单备注
private String note;
}
5.对应常量构建
package com.yueluo.mall.common.constant.order;
/**
* 订单相关常量定义
* @author xjx
* @email 15340655443@163.com
* @date 2024/2/15 16:17
*/
public class OrderConstant {
/**
* 订单的防重令牌前缀
* 格式:order:token:memberId 用于防重复提交
*/
public static final String ORDER_TOKEN_PREFIX = "order:token:";
}
package com.yueluo.mall.common.constant.order;
/**
* 订单枚举类
* @author xjx
* @email 15340655443@163.com
* @date 2024/1/10 19:35
*/
public enum OrderEnum {
/**
* 订单常量定义
*/
//支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
PAY_TYPE_ALIPAY(1,"支付宝"),
PAY_TYPE_WECHAT(2,"微信"),
PAY_TYPE_UNIONPAY(3,"银联"),
PAY_TYPE_CASH_ON_DELIVERY(4,"货到付款"),
//订单来源[0->PC订单;1->app订单]
ORDER_SOURCE_PC(0,"PC订单"),
ORDER_SOURCE_APP(1,"app订单"),
//订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
ORDER_STATUS_UNPAID(0,"待付款"),
ORDER_STATUS_UNDELIVERED(1,"待发货"),
ORDER_STATUS_DELIVERED(2,"已发货"),
ORDER_STATUS_COMPLETED(3,"已完成"),
ORDER_STATUS_CLOSED(4,"已关闭"),
ORDER_STATUS_INVALID(5,"无效订单"),
//发票类型[0->不开发票;1->电子发票;2->纸质发票]
INVOICE_TYPE_NONE(0,"不开发票"),
INVOICE_TYPE_ELECTRONIC(1,"电子发票"),
INVOICE_TYPE_PAPER(2,"纸质发票"),
//确认收货状态[0->未确认;1->已确认]
CONFIRM_STATUS_UNCONFIRMED(0,"未确认"),
CONFIRM_STATUS_CONFIRMED(1,"已确认"),
//删除状态【0->未删除;1->已删除】
DELETE_STATUS_UNDELETED(0,"未删除"),
DELETE_STATUS_DELETED(1,"已删除");
private int code;
private String msg;
OrderEnum(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMessage() {
return msg;
}
}
6.对应Controller
package com.yueluo.mall.order.web;
import com.yueluo.mall.common.exception.BugCodeEnume;
import com.yueluo.mall.common.exception.NoStockException;
import com.yueluo.mall.order.service.OrderService;
import com.yueluo.mall.order.vo.OrderConfirmVo;
import com.yueluo.mall.order.vo.OrderResponseVo;
import com.yueluo.mall.order.vo.OrderSubmitVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* 订单web控制器
* @author xjx
* @email 15340655443@163.com
* @date 2024/2/13 16:55
*/
@Controller
public class OrderWebController {
@Autowired
private OrderService orderService;
/**
* 去订单确认页
* @param model
* @return
*/
@GetMapping("/order/toTrade")
public String toTrade(Model model) {
//查询订单确认信息
OrderConfirmVo confirmVo=orderService.toConfirm();
model.addAttribute("confirmVo",confirmVo);
return "confirm";
}
/**
* 提交订单--->订单确认页点击提交订单
* 进入支付页
* @param submitVo
* @param model
* @param redirectAttributes
* @return
*/
@PostMapping("/order/submitOrder")
public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes) {
OrderResponseVo responseVo=null;
try{
//提交订单
responseVo= orderService.submitOrder(submitVo);
if (responseVo.getCode() != 0) {
String msg = "下单失败:"+responseVo.getMsg();
redirectAttributes.addFlashAttribute("msg",msg);
//订单提交失败跳转到订单确认页重新确认订单信息
return "redirect:http://www.yueluo.top/order/toTrade";
}
//下单成功跳转到支付页
model.addAttribute("responseVo",responseVo);
return "pay";
}catch (Exception exception){
exception.printStackTrace();
String msg = "下单失败:"+exception.getMessage();
redirectAttributes.addFlashAttribute("msg",msg);
//订单提交失败跳转到订单确认页重新确认订单信息
return "redirect:http://www.yueluo.top/order/toTrade";
}
}
}
7.复杂的业务处理
/**
* 提交订单--通过redis脚本来保证幂等性
* @param submitVo
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public OrderResponseVo submitOrder(OrderSubmitVo submitVo) throws NoStockException{
System.out.println("提交的订单信息:"+submitVo);
OrderResponseVo responseVo = new OrderResponseVo();
MemberDTO memberDTO = getMemberInfo();
String key= OrderConstant.ORDER_TOKEN_PREFIX+ memberDTO.getId();
String orderToken = submitVo.getOrderToken();
//1.验证令牌--验证成功删除令牌
String token = (String) redisTemplate.opsForValue().get(key);
//准备redis脚本
String script="if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = (Long) redisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(key),
orderToken);
if (result==0){
//令牌验证失败--说明订单信息已经提交过了
responseVo.setCode(BugCodeEnume.ORDER_TOKEN_EXCEPTION.getCode());
responseVo.setMsg(BugCodeEnume.ORDER_TOKEN_EXCEPTION.getMessage());
return responseVo;
}
//2.创建订单-订单项信息
OrderCreateDTO orderCreateDTO= createOrder(submitVo);
System.out.println("订单及其订单项信息:"+orderCreateDTO.toString());
//设置订单信息
responseVo.setOrderEntity(orderCreateDTO.getOrderEntity());
//3.下单--保存订单
saveOrder(orderCreateDTO);
//4.锁定库存信息
WareSkuLockDTO wareSkuLockDTO = new WareSkuLockDTO();
wareSkuLockDTO.setOrderSn(orderCreateDTO.getOrderEntity().getOrderSn());
List<com.yueluo.mall.common.vo.OrderItemVo> orderItemVos = orderCreateDTO.getOrderItemEntity().stream().map(orderItemEntity -> {
com.yueluo.mall.common.vo.OrderItemVo orderItemVo = new com.yueluo.mall.common.vo.OrderItemVo();
orderItemVo.setSkuId(orderItemEntity.getSkuId());
orderItemVo.setTitle(orderItemEntity.getSkuName());
orderItemVo.setCount(orderItemEntity.getSkuQuantity());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockDTO.setItemVos(orderItemVos);
//5.远程调用扣减库存
R skuStockResults = wareFeignService.lockStock(wareSkuLockDTO);
if (skuStockResults.getCode() != 0){
//锁定库存失败
responseVo.setCode(BugCodeEnume.NO_STOCK_EXCEPTION.getCode());
responseVo.setMsg(BugCodeEnume.NO_STOCK_EXCEPTION.getMessage());
//事务回滚
throw new NoStockException();
}
List<LockSkuStockResult> skuStockResultList = (List<LockSkuStockResult>) skuStockResults.get("skuStockResults");
System.out.println("下单成功对应的商品锁定库存信息反馈:"+skuStockResultList);
//6.远程调用扣减积分
//7.远程调用扣减优惠券
//8.远程调用扣减余额
//下单成功
responseVo.setCode(0);
responseVo.setMsg("下单成功");
return responseVo;
}
/**
* 保存订单
* @param orderCreateDTO
*/
private void saveOrder(OrderCreateDTO orderCreateDTO) {
//保存订单
OrderEntity orderEntity = orderCreateDTO.getOrderEntity();
this.save(orderEntity);
//保存订单项
List<OrderItemEntity> orderItemEntities = orderCreateDTO.getOrderItemEntity();
orderItemEntities.forEach(orderItemEntity -> {
orderItemEntity.setOrderId(orderEntity.getId());
});
//批量保存
orderItemService.saveBatch(orderItemEntities);
//TODO 保存订单操作日志
}
/**
* 创建订单
* @param submitVo
* @return
*/
private OrderCreateDTO createOrder(OrderSubmitVo submitVo) {
MemberDTO memberDTO = getMemberInfo();
OrderCreateDTO orderCreateDTO = new OrderCreateDTO();
//1.构建订单
OrderEntity orderEntity = buildOrder(submitVo);
orderCreateDTO.setOrderEntity(orderEntity);
//2.构建订单项
List<OrderItemVo> cartItems = cartFeignService.getCartItems(memberDTO.getId());
List<OrderItemSpuInfoVo> orderItemSpuInfoVos = productFeignService.getOrderItemSpuInfoVos
(cartItems.stream().map(OrderItemVo::getSpuId).collect(Collectors.toList()));
Map<Long, OrderItemSpuInfoVo> spuInfoVoMap = orderItemSpuInfoVos.stream()
.collect(Collectors.toMap(OrderItemSpuInfoVo::getSpuId, orderItemSpuInfoVo -> orderItemSpuInfoVo));
List<OrderItemEntity> orderItemEntities = cartItems.stream().map(item -> {
OrderItemEntity orderItemEntity = buildOrderItem(item,orderEntity,spuInfoVoMap.get(item.getSpuId()));
return orderItemEntity;
}).collect(Collectors.toList());
orderCreateDTO.setOrderItemEntity(orderItemEntities);
return orderCreateDTO;
}
/**
* 构建订单项
* @param orderEntity
* @return
*/
private OrderItemEntity buildOrderItem(OrderItemVo itemVo,OrderEntity orderEntity,OrderItemSpuInfoVo spuInfoVo) {
OrderItemEntity orderItemEntity = new OrderItemEntity();
//设置订单项的基本信息
orderItemEntity.setOrderId(orderEntity.getId());
//设置订单项的订单号
orderItemEntity.setOrderSn(orderEntity.getOrderSn());
//Spu信息
//设置订单项商品的品牌名称
orderItemEntity.setSpuBrand(spuInfoVo.getBrandName());
//设置订单项的商品的spuId
orderItemEntity.setSpuId(itemVo.getSpuId());
//设置订单项的商品的spu名称
orderItemEntity.setSpuName(spuInfoVo.getSpuName());
//设置订单项的商品的spu图片
orderItemEntity.setSpuPic(spuInfoVo.getImgUrl());
//设置订单项的商品的分类id
orderItemEntity.setCategoryId(spuInfoVo.getCatalogId());
//设置订单项的商品的skuId
orderItemEntity.setSkuId(itemVo.getSkuId());
//设置订单项的商品的sku名称
orderItemEntity.setSkuName(itemVo.getTitle());
//设置订单项的商品的sku图片
orderItemEntity.setSkuPic(itemVo.getImage());
//设置订单项的商品的销售属性组合
List<String> skuAttr = itemVo.getSkuAttr();
String delimitedString = org.springframework.util.StringUtils.collectionToDelimitedString(skuAttr, ";");
orderItemEntity.setSkuAttrsVals(delimitedString);
//设置订单项的商品的sku价格
orderItemEntity.setSkuPrice(itemVo.getPrice());
//设置订单项的商品的购买数量
orderItemEntity.setSkuQuantity(itemVo.getCount());
//优惠券信息
//积分信息
orderItemEntity.setGiftIntegration(itemVo.getPrice().intValue());
//成长值
orderItemEntity.setGiftGrowth(itemVo.getPrice().intValue());
return orderItemEntity;
}
/**
* 构建订单实体
* @param submitVo
* @return
*/
private OrderEntity buildOrder(OrderSubmitVo submitVo) {
MemberDTO memberDTO = getMemberInfo();
OrderEntity orderEntity = new OrderEntity();
//设置订单的基本信息
orderEntity.setMemberId(memberDTO.getId());
orderEntity.setMemberUsername(memberDTO.getUsername());
//设置订单号
// String orderSn = System.currentTimeMillis() + memberDTO.getId().toString();
String orderSn = IdWorker.getTimeId();
orderEntity.setOrderSn(orderSn);
//根据收货地址id查询收货地址信息
MemberAddressVo address = memberFeignService.getAddressById(submitVo.getAddrId());
//设置订单的收货地址信息
orderEntity.setReceiverCity(address.getCity());
orderEntity.setReceiverDetailAddress(address.getDetailAddress());
orderEntity.setReceiverName(address.getName());
orderEntity.setReceiverPhone(address.getPhone());
orderEntity.setReceiverPostCode(address.getPostCode());
orderEntity.setReceiverProvince(address.getProvince());
orderEntity.setReceiverRegion(address.getRegion());
//设置订单的支付方式--支付宝、微信、银联、货到付款
orderEntity.setPayType(submitVo.getPayType());
//设置订单来源--默认PC订单
orderEntity.setSourceType(OrderEnum.ORDER_SOURCE_PC.getCode());
//设置订单状态--待付款
orderEntity.setStatus(OrderEnum.ORDER_STATUS_UNPAID.getCode());
//设置订单的删除状态--未删除
orderEntity.setDeleteStatus(OrderEnum.DELETE_STATUS_UNDELETED.getCode());
//设置订单的确认状态--未确认
orderEntity.setConfirmStatus(OrderEnum.CONFIRM_STATUS_UNCONFIRMED.getCode());
//设置订单的发票类型--不开发票
orderEntity.setBillType(OrderEnum.INVOICE_TYPE_NONE.getCode());
//设置订单的总金额
orderEntity.setTotalAmount(submitVo.getTotal());
//设置订单的应付总金额
orderEntity.setPayAmount(submitVo.getPayTotal());
//设置订单的运费金额
orderEntity.setFreightAmount(new BigDecimal(0));
//设置订单的促销优化金额
//设置订单的积分抵扣金额
//设置订单的优惠券抵扣金额
//设置订单的后台调整订单使用的折扣金额
//设置订单的创建时间
orderEntity.setCreateTime(new Date());
//设置订单的使用的优惠券
//设置订单的支付时间
//设置订单的发货时间
//设置订单的确认收货时间
//设置订单的删除时间
//设置订单的自动确认时间
//设置订单的自动删除时间
return orderEntity;
}
8.锁定库存
8.1Vo构建
package com.yueluo.mall.common.dto;
import com.yueluo.mall.common.vo.OrderItemVo;
import lombok.Data;
import java.util.List;
/**
* 锁定库存信息
* @author xjx
* @email 15340655443@163.com
* @date 2024/2/16 8:20
*/
@Data
public class WareSkuLockDTO {
//订单号
private String orderSn;
//需要锁定的商品信息
private List<OrderItemVo> itemVos;
}
8.2 Controller
/**
* 锁定库存
* @param wareSkuLockDTO
* @return
*/
@PostMapping("/sku/lockStock")
public R lockStock(@RequestBody WareSkuLockDTO wareSkuLockDTO){
List<LockSkuStockResult> skuStockResults=null;
try {
skuStockResults=wareSkuService.lockStock(wareSkuLockDTO);
return R.ok().put("skuStockResults",skuStockResults);
} catch (Exception e) {
e.printStackTrace();
return R.error(BugCodeEnume.NO_STOCK_EXCEPTION.getCode(),BugCodeEnume.NO_STOCK_EXCEPTION.getMessage());
}
}
8.3 锁定库存
/**
* 锁定库存
* @param wareSkuLockDTO
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public List<LockSkuStockResult> lockStock(WareSkuLockDTO wareSkuLockDTO) {
//1.保存库存
List<OrderItemVo> itemVos = wareSkuLockDTO.getItemVos();
//2.找到具有可用库存的仓库
List<Long> skuIds = itemVos.stream().map((item) -> {
return item.getSkuId();
}).collect(Collectors.toList());
List<SkuWareHasStockVo> skuWareHasStockVos = itemVos.stream().map((item) -> {
SkuWareHasStockVo skuWareHasStockVo = new SkuWareHasStockVo();
skuWareHasStockVo.setSkuId(item.getSkuId());
skuWareHasStockVo.setStock(item.getCount());
//查询具有可用库存的仓库
List<WareSkuEntity> wareSkuEntities = wareSkuDao.listWareIdHasStock(item.getSkuId());
skuWareHasStockVo.setWareSkuEntities(wareSkuEntities);
return skuWareHasStockVo;
}).collect(Collectors.toList());
//3.锁定库存
List<LockSkuStockResult> lockSkuStockResults = new ArrayList<>();
for (SkuWareHasStockVo skuWareHasStockVo : skuWareHasStockVos) {
LockSkuStockResult lockSkuStockResult = new LockSkuStockResult();
lockSkuStockResult.setSkuId(skuWareHasStockVo.getSkuId());
lockSkuStockResult.setLockCount(skuWareHasStockVo.getStock());
//锁定库存
//需要锁定的库存数量
Integer needLockStock = skuWareHasStockVo.getStock();
if (skuWareHasStockVo.getWareSkuEntities() == null && skuWareHasStockVo.getWareSkuEntities().size() < 0) {
//没有可用库存
lockSkuStockResult.setLock(false);
lockSkuStockResult.setMsg("商品编号:" + skuWareHasStockVo.getSkuId() + "库存不足");
lockSkuStockResult.setNeedToPickStockCount(needLockStock);
lockSkuStockResults.add(lockSkuStockResult);
throw new NoStockException(skuWareHasStockVo.getSkuId(),needLockStock);
}
//有可用库存--尝试锁定库存
List<WareSkuEntity> wareSkuEntities = skuWareHasStockVo.getWareSkuEntities();
if (wareSkuEntities==null||wareSkuEntities.size()<=0){
lockSkuStockResult.setLock(false);
lockSkuStockResult.setMsg("商品编号:" + skuWareHasStockVo.getSkuId() + "库存不足");
lockSkuStockResult.setNeedToPickStockCount(needLockStock);
lockSkuStockResults.add(lockSkuStockResult);
throw new NoStockException(skuWareHasStockVo.getSkuId(),needLockStock);
}
for (WareSkuEntity wareSkuEntity : skuWareHasStockVo.getWareSkuEntities()) {
//能锁定的库存数量
int canCount= wareSkuEntity.getStock() - wareSkuEntity.getStockLocked();
if (canCount<=0){
continue;
}
if (canCount>=needLockStock){
//表示库存充足足以锁定--直接锁定库存即可
wareSkuDao.updateLockStock(wareSkuEntity.getId(),needLockStock);
lockSkuStockResult.setLock(true);
lockSkuStockResult.setMsg("商品编号:" + skuWareHasStockVo.getSkuId() + "库存锁定成功");
//将需要锁定的库存数量置为0
needLockStock=0;
lockSkuStockResult.setNeedToPickStockCount(needLockStock);
lockSkuStockResults.add(lockSkuStockResult);
}
//部分锁定--按照已有库存进行锁定
if (canCount>0&&canCount<needLockStock){
//库存不足--部分锁定
wareSkuDao.updateLockStock(wareSkuEntity.getId(),canCount);
//减去已锁定的库存
needLockStock-=canCount;
}
//继续循环找到下一个仓库进行锁定
}
if (needLockStock>0){
//库存不足
lockSkuStockResult.setLock(false);
lockSkuStockResult.setMsg("商品编号:" + skuWareHasStockVo.getSkuId() + "库存不足");
lockSkuStockResult.setNeedToPickStockCount(needLockStock);
lockSkuStockResults.add(lockSkuStockResult);
throw new NoStockException(skuWareHasStockVo.getSkuId(),needLockStock);
}
}
return lockSkuStockResults;
}
9.注意其中涉及到的循环查询数据库解决
可以查询列表信息用stream流转化为Map集合在通过循环来赋值即可
三、分布式事务
我们在分布式环境下一个业务可能会涉及到多个模块之间的调用,为了保证操作的原子性,分布式事务是最好的解决方案。
1.本地事务
在系统介绍分布式事务之前,我们还是很有必要回顾下本地事务。在一个服务中生效的事务我们称为本地事务。
1.1事务的特性
事务的概念:事务是逻辑上一组操作,组成这组操作各个逻辑单元,要么一起成功,要么一起失败。
事务的四个特性(ACID):
原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
一致性(consistency):
一致
指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
1.2事务的隔离级别
事务并发引起一些读的问题:
脏读 一个事务可以读取另一个事务未提交的数据
不可重复读 一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配
虚读(幻读) 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点
并发写:使用mysql默认的锁机制(独占锁)
解决读问题:设置事务隔离级别
read uncommitted(0)
read committed(2)
repeatable read(4)
Serializable(8)
隔离级别越高,性能越低
一般情况下:脏读是不可允许的,不可重复读和幻读是可以被适当允许的。
1.3事务的传播性
Spring中的7个事务传播行为:
事务行为 说明 PROPAGATION_REQUIRED 支持当前事务,假设当前没有事务。就新建一个事务 PROPAGATION_SUPPORTS 支持当前事务,假设当前没有事务,就以非事务方式运行 PROPAGATION_MANDATORY 支持当前事务,假设当前没有事务,就抛出异常 PROPAGATION_REQUIRES_NEW 新建事务,假设当前存在事务。把当前事务挂起 PROPAGATION_NOT_SUPPORTED 以非事务方式运行操作。假设当前存在事务,就把当前事务挂起 PROPAGATION_NEVER 以非事务方式运行,假设当前存在事务,则抛出异常 PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
1.4举例说明
ServiceA
ServiceA {
void methodA() {
ServiceB.methodB();
}
}
ServiceB
ServiceB {
void methodB() {
}
}
1.4.1PROPAGATION_REQUIRED
假如当前正要运行的事务不在另外一个事务里,那么就起一个新的事务 比方说,ServiceB.methodB的事务级别定义PROPAGATION_REQUIRED, 那么因为执行ServiceA.methodA的时候,ServiceA.methodA已经起了事务。这时调用ServiceB.methodB,ServiceB.methodB看到自己已经执行在ServiceA.methodA的事务内部。就不再起新的事务。而假如ServiceA.methodA执行的时候发现自己没有在事务中,他就会为自己分配一个事务。这样,在ServiceA.methodA或者在ServiceB.methodB内的不论什么地方出现异常。事务都会被回滚。即使ServiceB.methodB的事务已经被提交,可是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚
1.4.2PROPAGATION_SUPPORTS
假设当前在事务中。即以事务的形式执行。假设当前不在一个事务中,那么就以非事务的形式执行
1.4.3 PROPAGATION_MANDATORY
必须在一个事务中执行。也就是说,他仅仅能被一个父事务调用。否则,他就要抛出异常
1.4.4PROPAGATION_REQUIRES_NEW
这个就比较绕口了。 比方我们设计ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_REQUIRES_NEW。那么当运行到ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起。ServiceB.methodB会起一个新的事务。等待ServiceB.methodB的事务完毕以后,他才继续运行。 他与PROPAGATION_REQUIRED 的事务差别在于事务的回滚程度了。由于ServiceB.methodB是新起一个事务,那么就是存在两个不同的事务。假设ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚。ServiceB.methodB是不会回滚的。假设ServiceB.methodB失败回滚,假设他抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可能提交。
1.4.5PROPAGATION_NOT_SUPPORTED
当前不支持事务。比方ServiceA.methodA的事务级别是PROPAGATION_REQUIRED 。而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,那么当执行到ServiceB.methodB时。ServiceA.methodA的事务挂起。而他以非事务的状态执行完,再继续ServiceA.methodA的事务。
1.4.6PROPAGATION_NEVER
不能在事务中执行。 如果ServiceA.methodA的事务级别是PROPAGATION_REQUIRED。 而ServiceB.methodB的事务级别是PROPAGATION_NEVER ,那么ServiceB.methodB就要抛出异常了。
1.4.7PROPAGATION_NESTED
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
@Transactional(propagation=Propagation.REQUIRED) 如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED) 容器不为这个方法开启事务 @Transactional(propagation=Propagation.REQUIRES_NEW) 不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务 @Transactional(propagation=Propagation.MANDATORY) 必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.NEVER) 必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反) @Transactional(propagation=Propagation.SUPPORTS) 如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务
1.5SpringBoot事务代理对象
在SpringBoot中如果一个对象中有多个事务方法相互调用,那么事务传播会失效,主要原因是当前对象直接调用了自身对象的方法,绕过了代理对象的处理,造成了事务传播的失效。那么对应的解决方案是
spring-boot-stater-aop
来显示的获取代理对象来调用
1.5.1引入相关的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.4.12</version>
</dependency>
1.5.2添加aspectj的注解
然后在需要调用的位置通过
AopContext
获取当前的代理对象
/**
* 在service中调用自身的其他事务方法的时候,事务的传播行为会失效
* 因为会绕过代理对象的处理
*
*/
@Transactional // 事务A
public void a(){
OrderServiceImpl o = (OrderServiceImpl) AopContext.currentProxy();
o.b(); // 事务A
o.c(); // 事务C
int a = 10/0;
}
@Transactional(propagation = Propagation.REQUIRED)
public void b(){
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c(){
}
2.分布式事务
2.1分布式事务基础
2.1.1CAP定理
分布式存储系统的CAP原理(分布式系统的三个指标):
Consistency(一致性):在分布式系统中的所有数据备份,在同一时刻是否同样的值。
对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
Availability(可用性):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(要求数据需要备份)
Partition tolerance(分区容忍性):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们无法避免的。所以我们只能在一致性和可用性之间进行权衡,没有系统能同时保证这三点。要么选择CP、要么选择AP
2.1.2BASE定理
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。接下来看看BASE中的三要素:
Basically Available(基本可用)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。 电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
Soft state(软状态)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
Eventually consistent(最终一致性)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
BASE模型是传统ACID模型的反面,不同于ACID,BASE强调牺牲高一致性,从而获得可用性,数据允许在一段时间内的不一致,只要保证最终一致就可以了
3.分布式事务解决方案
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。
主流的解决方案如下:
基于XA协议的两阶段提交(2PC)
柔性事务-TCC事务
柔性事务-最终一致性
3.1两阶段提交(2PC)
2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.
第二阶段:事务协调器要求每个数据库提交数据。
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
目前主流数据库均支持2PC【2 Phase Commit】
XA 是一个两阶段提交协议,又叫做 XA Transactions。
MySQL从5.5版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。
总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。
两阶段提交涉及多次节点间的网络通信,通信时间太长!
事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多。
XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换会导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
3.2TCC补偿式事务
TCC 是一种编程式分布式事务解决方案。
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
Try:主要是对业务系统做检测及资源预留
Confirm:真正执行业务,不作任何业务检查;只使用Try阶段预留的业务资源;Confirm操作满足幂等性。
Cancel:释放Try阶段预留的业务资源;Cancel操作满足幂等性。
整个TCC业务分成两个阶段完成
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。
第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。
举个例子,假如 Bob 要向 Smith 转账100元,思路大概是:
我们有一个本地方法,里面依次调用
首先在 Try 阶段,要先检查Bob的钱是否充足,并把这100元锁住,Smith账户也冻结起来。
在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
缺点:
Canfirm和Cancel的幂等性很难保证。
这种方式缺点比较多,通常在复杂场景下是不推荐使用的,除非是非常简单的场景,非常容易提供回滚Cancel,而且依赖的服务也非常少的情况。
这种实现方式会造成代码量庞大,耦合性高。而且非常有局限性,因为有很多的业务是无法很简单的实现回滚的,如果串行的服务很多,回滚的成本实在太高。
不少大公司里,其实都是自己研发 TCC 分布式事务框架的,专门在公司内部使用。国内开源出去的:ByteTCC,TCC-transaction,Himly。
3.3消息事务+最终一致性
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。
适用于高并发最终一致
低并发基本一致:二阶段提交
高并发强一致:没有解决方案
4.seata
分布式事务解决方案seata
4.1seata服务的安装
我们可以先导入seata的依赖,根据依赖的版本来下载对应的seata安装文件
4.2配置nacos
下载后解压缩然后进入conf文件夹,然后通过registery.conf文件可以更新配置中心和注册中心的信息。
4.3启动服务
然后进入 bin 目录 通过 seata-server.bat文件来启动服务
然后进入nacos注册中心可以看到对应的服务,表示OK
4.4项目集成Seata
接下来看看如何在商城项目中来集成Seata,首先是file.conf文件
网络传输配置:
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
}
事务日志存储配置:该部分配置仅在seata-server中使用,如果选择db请配合seata.sql使用
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "file"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "mysql"
password = "mysql"
}
}
当前微服务在seata服务器中注册的信息配置:
service {
# 事务分组,默认:${spring.applicaiton.name}-fescar-service-group,可以随便写
# 注意看文档的参数配置 1.3.0以后采用驼峰命名所以是vgroupMapping
vgroup_mapping.${spring.application.name}-fescar-service-group = "default"
# 仅支持单节点,不要配置多地址,这里的default要和事务分组的值一致
default.grouplist = "127.0.0.1:8091" #seata-server服务器地址,默认是8091
# 降级,当前不支持
enableDegrade = false
# 禁用全局事务
disableGlobalTransaction = false
}
客户端相关工作的机制
client {
rm {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
retry.policy.branch-rollback-on-conflict = true
}
report.retry.count = 5
table.meta.check.enable = false
report.success.enable = true
}
tm {
commit.retry.count = 5
rollback.retry.count = 5
}
undo {
data.validation = true
log.serialization = "jackson"
log.table = "undo_log"
}
log {
exceptionRate = 100
}
support {
# auto proxy the DataSource bean
spring.datasource.autoproxy = false
}
}
4.5 步骤
首先我们需要把registry.conf和file.conf两个配置文件拷贝到对应的项目的属性文件目录中
然后我们在属性文件中定义 tx-service-group 信息
然后在file.conf中添加service属性,然后关联刚刚设置的tx-service-group信息。注意1.1版本后属性更新为了驼峰命名法:
4.6案例演示
我们在下订单的操作中除了已有的生成订单和订单项已经锁定库存操作外我们还显示的增加的了一个会有积分调整的服务。[1/0],这样一来如果锁定库存成功,但是会员积分调整失败,被分布式事务管理的逻辑中,锁库存的操作会回滚
/**
* 提交订单--通过redis脚本来保证幂等性
* 使用seata来实现分布式事务--@GlobalTransactional
* @param submitVo
* @return
*/
@GlobalTransactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
@Override
public OrderResponseVo submitOrder(OrderSubmitVo submitVo) throws NoStockException{
System.out.println("提交的订单信息:"+submitVo);
OrderResponseVo responseVo = new OrderResponseVo();
MemberDTO memberDTO = getMemberInfo();
String key= OrderConstant.ORDER_TOKEN_PREFIX+ memberDTO.getId();
String orderToken = submitVo.getOrderToken();
//1.验证令牌--验证成功删除令牌
String token = (String) redisTemplate.opsForValue().get(key);
String script="if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = (Long) redisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(key),
orderToken);
if (result==0){
//令牌验证失败--说明订单信息已经提交过了
responseVo.setCode(BugCodeEnume.ORDER_TOKEN_EXCEPTION.getCode());
responseVo.setMsg(BugCodeEnume.ORDER_TOKEN_EXCEPTION.getMessage());
return responseVo;
}
//2.创建订单-订单项信息
OrderCreateDTO orderCreateDTO= createOrder(submitVo);
System.out.println("订单及其订单项信息:"+orderCreateDTO.toString());
//设置订单信息
responseVo.setOrderEntity(orderCreateDTO.getOrderEntity());
//3.下单--保存订单
saveOrder(orderCreateDTO);
//4.锁定库存信息
lockSkuStock(orderCreateDTO, responseVo);
//6.远程调用扣减积分
// int i=1/0;//测试seata分布式事务
//7.远程调用扣减优惠券
//8.远程调用扣减余额
//下单成功
responseVo.setCode(0);
responseVo.setMsg("下单成功");
return responseVo;
}
5.下单流程梳理
6.取消订单 --解决方案
取消订单出现的情况:
下订单后超过30分钟没有支付,需要触发关单操作
支付失败,同样的需要关单
实现方式:定时任务和消息中间件,定时任务对系统的性能肯定是有影响
RocketMQ:https://github.com/apache/rocketmq/tree/master/docs/cn
7.RocketMQ
RocketMQ:Docker安装 : Docker+Mysql+Redis+Elaticsearch+Kibana+nginx+RocketMQ-CSDN博客
8.整合RocketMq
8.1 导入依赖
<-- springboot自动装配 !-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<-- RocketMq依赖 !-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.1</version>
</dependency>
8.2 配置
rocketmq: name-server: 127.0.0.1:9876 producer: group: order-group-producer consumer: group: order-group-consumer
8.3 定义生产者
package com.yueluo.mall.order.rocketmq;
import com.yueluo.mall.common.constant.order.OrderConstant;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
/**
* 订单服务的消息生产者
* @author xjx
* @email 15340655443@163.com
* @date 2024/2/17 10:24
*/
@Component
public class OrderMqProducer {
@Autowired
private RocketMQTemplate rocketMqTemplate;
/**
* 关闭订单的延迟消息
* @param orderSn 订单号
*/
public void sendOrderCreateMsg(String orderSn) {
rocketMqTemplate.syncSend(
OrderConstant.ROCKETMQ_ORDER_TOPIC,
MessageBuilder.withPayload(orderSn).build(),
5000,
4);
}
}
8.4 定义消费者
package com.yueluo.mall.order.rocketmq;
import com.yueluo.mall.common.constant.order.OrderConstant;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
* 订单服务的消息消费者
* @author xjx
* @email 15340655443@163.com
* @date 2024/2/17 10:51
*/
@RocketMQMessageListener(topic = OrderConstant.ROCKETMQ_ORDER_TOPIC, consumerGroup = "${rocketmq.consumer.group}")
@Component
public class OrderMqConsumer implements RocketMQListener<String> {
/**
* 消费消息
* @param orderSn
*/
@Override
public void onMessage(String orderSn) {
System.out.println("接收到消息-可以进行后续的关单操作-订单号为:" + orderSn);
}
}
9. 下单完成后发送延迟消息-以便关单操作
/**
* 提交订单--通过redis脚本来保证幂等性
* 使用seata来实现分布式事务--@GlobalTransactional
* @param submitVo
* @return
*/
@GlobalTransactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
@Override
public OrderResponseVo submitOrder(OrderSubmitVo submitVo) throws NoStockException{
System.out.println("提交的订单信息:"+submitVo);
OrderResponseVo responseVo = new OrderResponseVo();
MemberDTO memberDTO = getMemberInfo();
String key= OrderConstant.ORDER_TOKEN_PREFIX+ memberDTO.getId();
String orderToken = submitVo.getOrderToken();
//1.验证令牌--验证成功删除令牌
String token = (String) redisTemplate.opsForValue().get(key);
String script="if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = (Long) redisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(key),
orderToken);
if (result==0){
//令牌验证失败--说明订单信息已经提交过了
responseVo.setCode(BugCodeEnume.ORDER_TOKEN_EXCEPTION.getCode());
responseVo.setMsg(BugCodeEnume.ORDER_TOKEN_EXCEPTION.getMessage());
return responseVo;
}
//2.创建订单-订单项信息
OrderCreateDTO orderCreateDTO= createOrder(submitVo);
System.out.println("订单及其订单项信息:"+orderCreateDTO.toString());
//设置订单信息
responseVo.setOrderEntity(orderCreateDTO.getOrderEntity());
//3.下单--保存订单
saveOrder(orderCreateDTO);
//4.锁定库存信息
lockSkuStock(orderCreateDTO, responseVo);
//6.远程调用扣减积分
// int i=1/0;//测试seata分布式事务
//7.远程调用扣减优惠券
//8.远程调用扣减余额
//下单成功
responseVo.setCode(0);
responseVo.setMsg("下单成功");
//9.发送延迟30分钟的关单消息到mq
orderMqProducer.sendOrderCreateMsg(orderCreateDTO.getOrderEntity().getOrderSn());
return responseVo;
}