当前位置:   article > 正文

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_谷粒商城前端笔记

谷粒商城前端笔记

导航:

谷粒商城笔记+踩坑汇总篇

目录

1、环境准备

1.1、业务流程

1.2、Controller 层编写下单功能接口

1.3、订单提交的模型类

1.4、前端页面 confirm.html 提供数据

2、提交订单业务完整代码

3、原子性验令牌:令牌的对比和删除保证原子性

4、初始化新订单,包含订单、订单项等信息

4.1、抽取订单创建传输类

4.2、service

4.3、创建订单

4.3.1、远程调用仓库服务,计算运费和详细地址的接口

4.3.2、封装运费模型类

4.3.3、创建订单service

4.4、构造订单项数据

4.4.1、构建订单项数据service

4.4.2、【商品模块】通过skuId查询spu信息

4.4.3、订单服务远程调用商品服务

4.4.4、抽取商品信息vo

4.5、计算价格

5、锁定库存

5.1、保存订单数据并锁定库存

5.1.1、service保存订单数据并锁定库存

5.1.2、【公共模块】无库存异常类

5.2、【仓库模块】锁定库存

5.2.1、订单服务远程调用仓库服务

5.2.2、锁定库存controller

5.2.3、锁定库存的vo类

5.2.4、锁定指定订单的库存service

5.2.5、dao,根据sku_id查询在有库存的仓库

5.2.6、【公共模块】错误码枚举类添加库存相关错误码

6、前端页面的修改

7、提交订单的完整代码

7.1、Controller层接口编写

7.2、Service层代码

7.2.1、提交订单业务

7.2.2、创建订单、构建订单、计算价格等调用的方法

8、分布式事务优化

8.1、解决低并发场景的分布式事务

8.1.1 Seata的AT模式

8.1.2【商品模块】保存spu信息业务设为分布式事务  

8.2、解决低并发场景的分布式事务

8.2.1、可靠消息+最终一致性方案

8.2.2、若订单失败,自动解锁库存


1、环境准备

1.1、业务流程

img

1.2、Controller 层编写下单功能接口

订单服务 com.atguigu.gulimall.order.web 路径下的 OrderWebController 类,代码如下

  1. /**
  2. * 下单功能
  3. * @param vo
  4. * @return
  5. */
  6. @PostMapping("/submitOrder")
  7. public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
  8. // 1、创建订单、验令牌、验价格、验库存
  9. try {
  10. SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
  11. if (responseVo.getCode() == 0) {
  12. // 下单成功来到支付选择页
  13. model.addAttribute("submitOrderResp",responseVo);
  14. return "pay";
  15. } else {
  16. // 下单失败回到订单确认页重新确认订单信息
  17. String msg = "下单失败: ";
  18. switch ( responseVo.getCode()){
  19. case 1: msg+="订单信息过期,请刷新再次提交";break;
  20. case 2: msg+="订单商品价格发生变化,请确认后再次提交";break;
  21. case 3: msg+="商品库存不足";break;
  22. }
  23. redirectAttributes.addAttribute("msg",msg);
  24. return "redirect:http://order.gulimall.cn/toTrade";
  25. }
  26. } catch (Exception e){
  27. if (e instanceof NoStockException) {
  28. String message = e.getMessage();
  29. redirectAttributes.addFlashAttribute("msg", message);
  30. }
  31. return "redirect:http://order.gulimall.cn/toTrade";
  32. }
  33. }

1.3、订单提交的模型类


页面提交数据 添加“com.atguigu.gulimall.order.vo.OrderSubmitVo”类,代码如下:

  1. @Data
  2. @ToString
  3. public class OrderSubmitVo {
  4. /**
  5. * 收货地址Id
  6. */
  7. private Long addrId;
  8. /**
  9. * 支付方式
  10. */
  11. private Integer payType;
  12. // 无需提交需要购买的商品,去购物车再获取一遍
  13. // 优惠发票
  14. /**
  15. * 防重令牌
  16. */
  17. private String orderToken;
  18. /**
  19. * 应付价格,验价
  20. */
  21. private BigDecimal payPrice;
  22. /**
  23. * 订单备注
  24. */
  25. private String note;
  26. /**
  27. * 用户相关信息,直接去Session取出登录的用户
  28. */
  29. }

1.4、前端页面 confirm.html 提供数据

  1. <form action="http://order.gulimall.cn/submitOrder" method="post">
  2. <input id="addrIdInput" type="hidden" name="addrId">
  3. <input id="payPriceInput" type="hidden" name="payPrice">
  4. <input type="hidden" name="orderToken" th:value="${orderConfirmData.orderToken}">
  5. <button class="tijiao" type="submit">提交订单</button>
  6. </form>

  1. function getFare(addrId) {
  2. // 给表单回填的地址
  3. $("#addrIdInput").val(addrId);
  4. $.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
  5. console.log(resp);
  6. $("#fareEle").text(resp.data.fare);
  7. var total = [[${orderConfirmData.total}]]
  8. // 设置运费信息
  9. var pryPrice = total*1 + resp.data.fare*1;
  10. $("#payPriceEle").text(pryPrice);
  11. $("#payPriceInput").val(pryPrice);
  12. // 设置收货人信息
  13. $("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
  14. $("#reveiverEle").text(resp.data.address.name);
  15. })
  16. }

2、提交订单业务完整代码

  1. /**
  2. * 提交订单
  3. * @param vo
  4. * @return
  5. */
  6. // @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别
  7. // @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播级别
  8. @Transactional(rollbackFor = Exception.class)
  9. // @GlobalTransactional(rollbackFor = Exception.class)
  10. @Override
  11. public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
  12. confirmVoThreadLocal.set(vo);
  13. SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
  14. //去创建、下订单、验令牌、验价格、锁定库存...
  15. //1.从拦截器中获取当前用户登录的信息
  16. MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
  17. responseVo.setCode(0);
  18. //2、验证令牌是否合法【令牌的对比和删除必须保证原子性】
  19. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  20. String orderToken = vo.getOrderToken();
  21. //通过lure脚本原子验证令牌和删除令牌
  22. Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
  23. Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
  24. orderToken);
  25. if (result == 0L) {
  26. //令牌验证失败
  27. responseVo.setCode(1);
  28. return responseVo;
  29. } else {
  30. //令牌验证成功
  31. //1、创建订单、订单项等信息
  32. OrderCreateTo order = createOrder();
  33. //2、验证价格
  34. BigDecimal payAmount = order.getOrder().getPayAmount();
  35. BigDecimal payPrice = vo.getPayPrice();
  36. if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
  37. //金额对比
  38. //TODO 3、保存订单
  39. saveOrder(order);
  40. //4、库存锁定,只要有异常,回滚订单数据
  41. //订单号、所有订单项信息(skuId,skuNum,skuName)
  42. WareSkuLockVo lockVo = new WareSkuLockVo();
  43. lockVo.setOrderSn(order.getOrder().getOrderSn());
  44. //获取出要锁定的商品数据信息
  45. List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
  46. OrderItemVo orderItemVo = new OrderItemVo();
  47. orderItemVo.setSkuId(item.getSkuId());
  48. orderItemVo.setCount(item.getSkuQuantity());
  49. orderItemVo.setTitle(item.getSkuName());
  50. return orderItemVo;
  51. }).collect(Collectors.toList());
  52. lockVo.setLocks(orderItemVos);
  53. //TODO 调用远程锁定库存的方法
  54. //出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
  55. //为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
  56. R r = wmsFeignService.orderLockStock(lockVo);
  57. if (r.getCode() == 0) {
  58. //锁定成功
  59. responseVo.setOrder(order.getOrder());
  60. // int i = 10/0;
  61. //TODO 订单创建成功,发送消息给MQ
  62. rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
  63. //删除购物车里的数据
  64. redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
  65. return responseVo;
  66. } else {
  67. //锁定失败
  68. String msg = (String) r.get("msg");
  69. throw new NoStockException(msg);
  70. // responseVo.setCode(3);
  71. // return responseVo;
  72. }
  73. } else {
  74. responseVo.setCode(2);
  75. return responseVo;
  76. }
  77. }
  78. }

3、原子性验令牌:令牌的对比和删除保证原子性


问题:存在网路延时,同时提交从Redis拿到的令牌一直,导致重复提交

解决:令牌的对比和删除必须保证原子性

1)、封装提交订单数据

  1. package com.atguigu.gulimall.order.vo;
  2. @Data
  3. public class SubmitOrderResponseVo {
  4. private OrderEntity order;
  5. private Integer code; //0成功,错误状态码
  6. }

2)、修改 SubmitOrderResponseVo 类编写验证令牌操作

  1. /**
  2. * 下单操作:验令牌、创建订单、验价格、验库存
  3. * @param vo
  4. * @return
  5. */
  6. @Override
  7. public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
  8. SubmitOrderResponseVo response = new SubmitOrderResponseVo();
  9. // 从拦截器中拿到当前的用户
  10. MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
  11. // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
  12. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  13. String orderToken = vo.getOrderToken();
  14. // 原子验证令牌和删除令牌
  15. Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(),
  16. OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), orderToken);
  17. if (result == 0L) {
  18. // 令牌验证失败
  19. response.setCode(1);
  20. return response;
  21. } else {
  22. // 令牌验证成功
  23. return response;
  24. }
  25. }

4、初始化新订单,包含订单、订单项等信息

4.1、抽取订单创建传输类

  1. @Data
  2. public class OrderCreateTo {
  3. private OrderEntity order;
  4. private List<OrderItemEntity> orderItems;
  5. /** 订单计算的应付价格 **/
  6. private BigDecimal payPrice;
  7. /** 运费 **/
  8. private BigDecimal fare;
  9. }

4.2、service

gulimall-order服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

  1. /**
  2. * 创建订单、订单项等信息
  3. * @return
  4. */
  5. private OrderCreateTo createOrder(){
  6. OrderCreateTo createTo = new OrderCreateTo();
  7. // 1、生成一个订单号。IdWorker.getTimeId()是Mybatis提供的生成订单号方法,ID=Time+Id
  8. String orderSn = IdWorker.getTimeId();
  9. // 2、构建一个订单
  10. OrderEntity orderEntity = buildOrder(orderSn);
  11. // 3、获取到所有的订单项
  12. List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
  13. // 4、计算价格、积分等相关信息
  14. computePrice(orderEntity,itemEntities);
  15. createTo.setOrder(orderEntity);
  16. createTo.setOrderItems(itemEntities);
  17. return createTo;
  18. }

4.3、创建订单

4.3.1、远程调用仓库服务,计算运费和详细地址的接口

  1. package com.atguigu.gulimall.order.feign;
  2. @FeignClient("gulimall-ware")
  3. public interface WareFeignService {
  4. @PostMapping("/ware/waresku/hasstock")
  5. R getSkusHasStock(@RequestBody List<Long> skuIds);
  6. /**
  7. * 计算运费和详细地址
  8. * @param addrId
  9. * @return
  10. */
  11. @GetMapping("/ware/wareinfo/fare")
  12. R getFare(@RequestParam("addrId") Long addrId);
  13. }

4.3.2、封装运费模型类

  1. package com.atguigu.gulimall.order.vo;
  2. @Data
  3. public class FareVo {
  4. private MemberAddressVo address;
  5. private BigDecimal fare;
  6. }

4.3.3、创建订单service

  1. /**
  2. * 构建订单
  3. * @param orderSn
  4. * @return
  5. */
  6. private OrderEntity buildOrder(String orderSn) {
  7. MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
  8. OrderEntity entity = new OrderEntity();
  9. entity.setOrderSn(orderSn);
  10. entity.setMemberId(respVp.getId());
  11. OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
  12. // 1、获取运费 和 收货信息
  13. R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
  14. FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
  15. });
  16. // 2、设置运费
  17. entity.setFreightAmount(fareResp.getFare());
  18. // 3、设置收货人信息
  19. entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
  20. entity.setReceiverProvince(fareResp.getAddress().getProvince());
  21. entity.setReceiverRegion(fareResp.getAddress().getRegion());
  22. entity.setReceiverCity(fareResp.getAddress().getCity());
  23. entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
  24. entity.setReceiverName(fareResp.getAddress().getName());
  25. entity.setReceiverPhone(fareResp.getAddress().getPhone());
  26. // 4、设置订单的相关状态信息
  27. entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
  28. // 5、默认取消信息
  29. entity.setAutoConfirmDay(7);
  30. return entity;
  31. }

4.4、构造订单项数据

4.4.1、构建订单项数据service

OrderServiceImpl 类

  1. /**
  2. * 构建所有订单项数据
  3. * @return
  4. */
  5. private List<OrderItemEntity> buildOrderItems(String orderSn) {
  6. // 最后确定每个购物项的价格
  7. List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
  8. if (currentUserCartItems != null && currentUserCartItems.size()>0){
  9. List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
  10. OrderItemEntity itemEntity = buildOrderItem(cartItem);
  11. itemEntity.setOrderSn(orderSn);
  12. return itemEntity;
  13. }).collect(Collectors.toList());
  14. return itemEntities;
  15. }
  16. return null;
  17. }
  18. /**
  19. * 构建某一个订单项
  20. * @param cartItem
  21. * @return
  22. */
  23. private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
  24. OrderItemEntity itemEntity = new OrderItemEntity();
  25. // 1、订单信息:订单号 v
  26. // 2、商品的spu信息
  27. Long skuId = cartItem.getSkuId();
  28. R r = productFeignService.getSpuInfoBySkuId(skuId);
  29. SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
  30. });
  31. itemEntity.setSpuId(data.getId());
  32. itemEntity.setSpuBrand(data.getBrandId().toString());
  33. itemEntity.setSpuName(data.getSpuName());
  34. itemEntity.setCategoryId(data.getCatalogId());
  35. // 3、商品的sku信息 v
  36. itemEntity.setSkuId(cartItem.getSkuId());
  37. itemEntity.setSkuName(cartItem.getTitle());
  38. itemEntity.setSkuPic(cartItem.getImage());
  39. itemEntity.setSkuPrice(cartItem.getPrice());
  40. itemEntity.setSkuQuantity(cartItem.getCount());
  41. itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
  42. // 4、优惠信息【不做】
  43. // 5、积分信息
  44. itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
  45. itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
  46. // 6、订单项的价格信息
  47. itemEntity.setPromotionAmount(new BigDecimal("0"));
  48. itemEntity.setCouponAmount(new BigDecimal("0"));
  49. itemEntity.setIntegrationAmount(new BigDecimal("0"));
  50. // 当前订单项的实际金额 总额-各种优惠
  51. BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
  52. BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
  53. subtract(itemEntity.getCouponAmount()).
  54. subtract(itemEntity.getIntegrationAmount());
  55. itemEntity.setRealAmount(subtract);
  56. return itemEntity;
  57. }

4.4.2、【商品模块】通过skuId查询spu信息

  1. package com.atguigu.gulimall.product.app;
  2. @RestController
  3. @RequestMapping("product/spuinfo")
  4. public class SpuInfoController {
  5. @Autowired
  6. private SpuInfoService spuInfoService;
  7. /**
  8. * 查询指定sku的spu信息
  9. * @param skuId
  10. * @return
  11. */
  12. @GetMapping("/skuId/{id}")
  13. public R getSpuInfoBySkuId(@PathVariable("id") Long skuId) {
  14. SpuInfoEntity entity = spuInfoService.getSpuInfoBySkuId(skuId);
  15. return R.ok().setData(entity);
  16. }

  1. package com.atguigu.gulimall.product.service.impl;
  2. @Override
  3. public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
  4. SkuInfoEntity byId = skuInfoService.getById(skuId);
  5. Long spuId = byId.getSpuId();
  6. SpuInfoEntity spuInfoEntity = getById(spuId);
  7. return spuInfoEntity;
  8. }

4.4.3、订单服务远程调用商品服务

  1. package com.atguigu.gulimall.order.feign;
  2. @FeignClient("gulimall-product")
  3. public interface ProductFeignService {
  4. @GetMapping("/product/spuinfo/skuId/{id}")
  5. R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
  6. }

4.4.4、抽取商品信息vo

  1. package com.atguigu.gulimall.order.vo;
  2. @Data
  3. public class SpuInfoVo {
  4. /**
  5. * 商品id
  6. */
  7. @TableId
  8. private Long id;
  9. /**
  10. * 商品名称
  11. */
  12. private String spuName;
  13. /**
  14. * 商品描述
  15. */
  16. private String spuDescription;
  17. /**
  18. * 所属分类id
  19. */
  20. private Long catalogId;
  21. /**
  22. * 品牌id
  23. */
  24. private Long brandId;
  25. /**
  26. *
  27. */
  28. private BigDecimal weight;
  29. /**
  30. * 上架状态[0 - 新建,1 - 上架,2-下架]
  31. */
  32. private Integer publishStatus;
  33. /**
  34. *
  35. */
  36. private Date createTime;
  37. /**
  38. *
  39. */
  40. private Date updateTime;
  41. }

4.5、计算价格

  1. /**
  2. * 计算价格
  3. * @param orderEntity
  4. * @param itemEntities
  5. */
  6. private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
  7. BigDecimal total = new BigDecimal("0.0");
  8. BigDecimal coupon = new BigDecimal("0.0");
  9. BigDecimal integration = new BigDecimal("0.0");
  10. BigDecimal promotion = new BigDecimal("0.0");
  11. BigDecimal gift = new BigDecimal("0.0");
  12. BigDecimal growth = new BigDecimal("0.0");
  13. // 1、订单的总额,叠加每一个订单项的总额信息
  14. for (OrderItemEntity entity : itemEntities) {
  15. total = total.add(entity.getRealAmount());
  16. coupon = coupon.add(entity.getCouponAmount());
  17. integration = integration.add(entity.getIntegrationAmount());
  18. promotion = promotion.add(entity.getPromotionAmount());
  19. gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
  20. growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
  21. }
  22. // 订单总额
  23. orderEntity.setTotalAmount(total);
  24. // 应付总额
  25. orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
  26. orderEntity.setCouponAmount(coupon);
  27. orderEntity.setIntegrationAmount(integration);
  28. orderEntity.setPromotionAmount(promotion);
  29. // 设置积分等信息
  30. orderEntity.setIntegration(gift.intValue());
  31. orderEntity.setGrowth(growth.intValue());
  32. orderEntity.setDeleteStatus(0);//0 未删除
  33. }

5、锁定库存

在这里插入图片描述

5.1、保存订单数据并锁定库存

5.1.1、service保存订单数据并锁定库存

  1. /**
  2. * 下单操作:验令牌、创建订单、验价格、验库存
  3. * @param vo
  4. * @return
  5. */
  6. @Transactional
  7. @Override
  8. public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
  9. // 在当前线程共享 OrderSubmitVo
  10. confirmVoThreadLocal.set(vo);
  11. SubmitOrderResponseVo response = new SubmitOrderResponseVo();
  12. // 从拦截器中拿到当前的用户
  13. MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
  14. response.setCode(0);
  15. // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
  16. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  17. String orderToken = vo.getOrderToken();
  18. // 原子验证令牌和删除令牌
  19. Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
  20. Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
  21. if (result == 0L) {
  22. // 令牌验证失败
  23. response.setCode(1);
  24. return response;
  25. } else {
  26. // 令牌验证成功
  27. // 2、创建订单、订单项等信息
  28. OrderCreateTo order = createOrder();
  29. // 3、验价
  30. BigDecimal payAmount = order.getOrder().getPayAmount();
  31. BigDecimal payPrice = vo.getPayPrice();
  32. if (Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
  33. // 金额对比成功
  34. // 4、保存订单;
  35. saveOrder(order);
  36. // 5、库存锁定,只要有异常回滚订单数据
  37. // 订单号,所有订单项(skuId,skuName,num)
  38. WareSkuLockVo lockVo = new WareSkuLockVo();
  39. lockVo.setOrderSn(order.getOrder().getOrderSn());
  40. List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
  41. OrderItemVo itemVo = new OrderItemVo();
  42. itemVo.setSkuId(item.getSkuId());
  43. itemVo.setCount(item.getSkuQuantity());
  44. itemVo.setTitle(item.getSkuName());
  45. return itemVo;
  46. }).collect(Collectors.toList());
  47. lockVo.setLocks(locks);
  48. // TODO 远程锁库存
  49. R r = wareFeignService.orderLockStock(lockVo);
  50. if (r.getCode() == 0) {
  51. // 锁成功了
  52. response.setOrder(order.getOrder());
  53. return response;
  54. }else {
  55. // 锁定失败
  56. throw new NoStockException((String) r.get("msg"));
  57. }
  58. } else {
  59. // 金额对比失败
  60. response.setCode(2);
  61. return response;
  62. }
  63. }
  64. }

5.1.2、【公共模块】无库存异常类

  1. package com.atguigu.common.exception;
  2. public class NoStockException extends RuntimeException{
  3. private Long skuId;
  4. public NoStockException(Long skuId){
  5. super("商品id:"+skuId+";没有足够的库存了!");
  6. }
  7. public NoStockException(String message) {
  8. super(message);
  9. }
  10. @Override
  11. public String getMessage() {
  12. return super.getMessage();
  13. }
  14. public Long getSkuId() {
  15. return skuId;
  16. }
  17. public void setSkuId(Long skuId) {
  18. this.skuId = skuId;
  19. }
  20. }

5.2、【仓库模块】锁定库存

5.2.1、订单服务远程调用仓库服务

  1. package com.atguigu.gulimall.order.feign;
  2. @FeignClient("gulimall-ware")
  3. public interface WareFeignService {
  4. //....
  5. /**
  6. * 锁定指定订单的库存
  7. * @param vo
  8. * @return
  9. */
  10. @PostMapping("/ware/waresku/lock/order")
  11. R orderLockStock(@RequestBody WareSkuLockVo vo);
  12. }

5.2.2、锁定库存controller

  1. package com.atguigu.gulimall.ware.controller;
  2. @RestController
  3. @RequestMapping("ware/waresku")
  4. public class WareSkuController {
  5. @Autowired
  6. private WareSkuService wareSkuService;
  7. /**
  8. * 锁定订单项库存
  9. * @param vo
  10. * @return
  11. */
  12. @PostMapping("/lock/order")
  13. public R orderLockStock(@RequestBody WareSkuLockVo vo){
  14. try {
  15. Boolean stock = wareSkuService.orderLockStock(vo);
  16. return R.ok();
  17. } catch (NoStockException e){
  18. return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());
  19. }
  20. }
  21. //....
  22. }

5.2.3、锁定库存的vo类

  1. /**
  2. * @Description: 锁定库存的vo
  3. **/
  4. @Data
  5. public class WareSkuLockVo {
  6. private String orderSn;
  7. /** 需要锁住的所有库存信息 **/
  8. private List<OrderItemVo> locks;
  9. }

5.2.4、锁定指定订单的库存service

  1. package com.atguigu.gulimall.ware.service.impl;
  2. @Service("wareSkuService")
  3. public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
  4. @Autowired
  5. WareSkuDao wareSkuDao;
  6. @Autowired
  7. ProductFeignService productFeignService;
  8. //......
  9. /**
  10. * 锁定指定订单的库存
  11. * @param vo
  12. * @return
  13. * (rollbackFor = NoStockException.class)
  14. * 默认只要是运行时异常都会回滚
  15. */
  16. @Transactional
  17. @Override
  18. public Boolean orderLockStock(WareSkuLockVo vo) {
  19. // 1、每个商品在哪个库存里有库存
  20. List<OrderItemVo> locks = vo.getLocks();
  21. List<SkuWareHashStock> collect = locks.stream().map(item -> {
  22. SkuWareHashStock stock = new SkuWareHashStock();
  23. Long skuId = item.getSkuId();
  24. stock.setSkuId(skuId);
  25. stock.setNum(item.getCount());
  26. // 查询这个商品在哪里有库存
  27. List<Long> wareIds = wareSkuDao.listWareIdHashSkuStock(skuId);
  28. stock.setWareId(wareIds);
  29. return stock;
  30. }).collect(Collectors.toList());
  31. // 2、锁定库存
  32. for (SkuWareHashStock hashStock : collect) {
  33. Boolean skuStocked = false;
  34. Long skuId = hashStock.getSkuId();
  35. List<Long> wareIds = hashStock.getWareId();
  36. if (wareIds == null || wareIds.size()==0){
  37. // 没有任何仓库有这个商品的库存
  38. throw new NoStockException(skuId);
  39. }
  40. for (Long wareId : wareIds) {
  41. // 成功就返回1,否则就返回0
  42. Long count = wareSkuDao.lockSkuStock(skuId,wareId,hashStock.getNum());
  43. if (count == 1){
  44. skuStocked = true;
  45. break;
  46. } else {
  47. // 当前仓库锁失败,重试下一个仓库
  48. }
  49. }
  50. if (skuStocked == false){
  51. // 当前商品所有仓库都没有锁住,其他商品也不需要锁了,直接返回没有库存了
  52. throw new NoStockException(skuId);
  53. }
  54. }
  55. // 3、运行到这,全部都是锁定成功的
  56. return true;
  57. }
  58. @Data
  59. class SkuWareHashStock{
  60. private Long skuId; // skuid
  61. private Integer num; // 锁定件数
  62. private List<Long> wareId; // 锁定仓库id
  63. }
  64. }

5.2.5、dao,根据sku_id查询在有库存的仓库

gulimall-ware服务中com.atguigu.gulimall.ware.dao路径下的 WareSkuDao 类:

  1. package com.atguigu.gulimall.ware.dao;
  2. @Mapper
  3. public interface WareSkuDao extends BaseMapper<WareSkuEntity> {
  4. /**
  5. * 通过skuId查询在哪个仓库有库存
  6. * @param skuId
  7. * @return 仓库的编号
  8. */
  9. List<Long> listWareIdHashSkuStock(@Param("skuId") Long skuId);
  10. /**
  11. * 锁库存
  12. * @param skuId
  13. * @param wareId
  14. * @param num
  15. * @return
  16. */
  17. Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);
  18. }

gulimall-ware服务中gulimall-ware/src/main/resources/mapper/ware路径下的 WareSkuDao.xml:

  1. <update id="addStock">
  2. UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
  3. </update>
  4. <select id="listWareIdHashSkuStock" resultType="java.lang.Long">
  5. SELECT ware_id FROM wms_ware_sku WHERE sku_id=#{skuId} and stock-stock_locked>0;
  6. </select>

5.2.6、【公共模块】错误码枚举类添加库存相关错误码

在 错误码和错误信息定义类 BizCodeEnume枚举类中新增 库存 错误码和信息

gulimall-common服务中com.atguigu.common.exception路径下的 BizCodeEnume:

以21开头的错误码: 库存

  1. package com.atguigu.common.exception;
  2. public enum BizCodeEnume {
  3. UNKNOW_EXCEPTION(10000,"系统未知异常"),
  4. VAILD_EXCEPTION(10001,"参数格式校验失败"),
  5. SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
  6. PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
  7. USER_EXIST_EXCEPTION(15001,"用户名已存在"),
  8. PHONE_EXIST_EXCEPTION(15002,"手机号已被注册"),
  9. NO_STOCK_EXCEPTION(21000,"商品库存不足"),
  10. LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误");
  11. private int code;
  12. private String msg;
  13. BizCodeEnume(int code,String msg){
  14. this.code = code;
  15. this.msg = msg;
  16. }
  17. public int getCode() {
  18. return code;
  19. }
  20. public String getMsg() {
  21. return msg;
  22. }
  23. }

6、前端页面的修改


订单提交成功,跳转到支付页面 pay.html

  1. <div class="Jdbox_BuySuc">
  2. <dl>
  3. <dt><img src="/static/order/pay/img/saoyisao.png" alt=""></dt>
  4. <dd>
  5. <span>订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]]</span>
  6. <span>应付金额<font>[[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]</font></span>
  7. </dd>
  8. <dd>
  9. <span>推荐使用</span>
  10. <span>扫码支付请您在<font>24小时</font>内完成支付,否则订单会被自动取消(库存紧订单请参见详情页时限)</span>
  11. <span>订单详细</span>
  12. </dd>
  13. </dl>
  14. </div>

在这里插入图片描述

订单提交失败,重定项到confirm.html 并回显 失败原因


<p class="p1">填写并核对订单信息 <span style="color: red" th:value="${msg!=null}" th:text="${msg}"></span></p>

在这里插入图片描述

在这里插入图片描述

7、提交订单的完整代码

7.1、Controller层接口编写

  1. /**
  2. * 下单功能
  3. * @param vo
  4. * @return
  5. */
  6. @PostMapping("/submitOrder")
  7. public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
  8. // 1、创建订单、验令牌、验价格、验库存
  9. try {
  10. SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
  11. if (responseVo.getCode() == 0) {
  12. // 下单成功来到支付选择页
  13. model.addAttribute("submitOrderResp",responseVo);
  14. return "pay";
  15. } else {
  16. // 下单失败回到订单确认页重新确认订单信息
  17. String msg = "下单失败: ";
  18. switch ( responseVo.getCode()){
  19. case 1: msg+="订单信息过期,请刷新再次提交";break;
  20. case 2: msg+="订单商品价格发生变化,请确认后再次提交";break;
  21. case 3: msg+="商品库存不足";break;
  22. }
  23. redirectAttributes.addAttribute("msg",msg);
  24. return "redirect:http://order.gulimall.cn/toTrade";
  25. }
  26. } catch (Exception e){
  27. if (e instanceof NoStockException) {
  28. String message = e.getMessage();
  29. redirectAttributes.addFlashAttribute("msg", message);
  30. }
  31. return "redirect:http://order.gulimall.cn/toTrade";
  32. }
  33. }

7.2、Service层代码

7.2.1、提交订单业务

  1. /**
  2. * 提交订单
  3. * @param vo
  4. * @return
  5. */
  6. // @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别
  7. // @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播级别
  8. @Transactional(rollbackFor = Exception.class)
  9. // @GlobalTransactional(rollbackFor = Exception.class)
  10. @Override
  11. public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
  12. confirmVoThreadLocal.set(vo);
  13. SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
  14. //去创建、下订单、验令牌、验价格、锁定库存...
  15. //1.从拦截器中获取当前用户登录的信息
  16. MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
  17. responseVo.setCode(0);
  18. //2、验证令牌是否合法【令牌的对比和删除必须保证原子性】
  19. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  20. String orderToken = vo.getOrderToken();
  21. //通过lure脚本原子验证令牌和删除令牌
  22. Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
  23. Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
  24. orderToken);
  25. if (result == 0L) {
  26. //令牌验证失败
  27. responseVo.setCode(1);
  28. return responseVo;
  29. } else {
  30. //令牌验证成功
  31. //1、创建订单、订单项等信息
  32. OrderCreateTo order = createOrder();
  33. //2、验证价格
  34. BigDecimal payAmount = order.getOrder().getPayAmount();
  35. BigDecimal payPrice = vo.getPayPrice();
  36. if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
  37. //金额对比
  38. //TODO 3、保存订单
  39. saveOrder(order);
  40. //4、库存锁定,只要有异常,回滚订单数据
  41. //订单号、所有订单项信息(skuId,skuNum,skuName)
  42. WareSkuLockVo lockVo = new WareSkuLockVo();
  43. lockVo.setOrderSn(order.getOrder().getOrderSn());
  44. //获取出要锁定的商品数据信息
  45. List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
  46. OrderItemVo orderItemVo = new OrderItemVo();
  47. orderItemVo.setSkuId(item.getSkuId());
  48. orderItemVo.setCount(item.getSkuQuantity());
  49. orderItemVo.setTitle(item.getSkuName());
  50. return orderItemVo;
  51. }).collect(Collectors.toList());
  52. lockVo.setLocks(orderItemVos);
  53. //TODO 调用远程锁定库存的方法
  54. //出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
  55. //为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
  56. R r = wmsFeignService.orderLockStock(lockVo);
  57. if (r.getCode() == 0) {
  58. //锁定成功
  59. responseVo.setOrder(order.getOrder());
  60. // int i = 10/0;
  61. //TODO 订单创建成功,发送消息给MQ
  62. rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
  63. //删除购物车里的数据
  64. redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
  65. return responseVo;
  66. } else {
  67. //锁定失败
  68. String msg = (String) r.get("msg");
  69. throw new NoStockException(msg);
  70. // responseVo.setCode(3);
  71. // return responseVo;
  72. }
  73. } else {
  74. responseVo.setCode(2);
  75. return responseVo;
  76. }
  77. }
  78. }

7.2.2、创建订单、构建订单、计算价格等调用的方法

  1. /**
  2. * 创建订单、订单项等信息
  3. * @return
  4. */
  5. private OrderCreateTo createOrder(){
  6. OrderCreateTo createTo = new OrderCreateTo();
  7. // 1、生成一个订单号
  8. String orderSn = IdWorker.getTimeId();
  9. // 2、构建一个订单
  10. OrderEntity orderEntity = buildOrder(orderSn);
  11. // 3、获取到所有的订单项
  12. List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
  13. // 4、计算价格、积分等相关信息
  14. computePrice(orderEntity,itemEntities);
  15. createTo.setOrder(orderEntity);
  16. createTo.setOrderItems(itemEntities);
  17. return createTo;
  18. }
  19. /**
  20. * 构建订单
  21. * @param orderSn
  22. * @return
  23. */
  24. private OrderEntity buildOrder(String orderSn) {
  25. MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
  26. OrderEntity entity = new OrderEntity();
  27. entity.setOrderSn(orderSn);
  28. entity.setMemberId(respVp.getId());
  29. OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
  30. // 1、获取运费 和 收货信息
  31. R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
  32. FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
  33. });
  34. // 2、设置运费
  35. entity.setFreightAmount(fareResp.getFare());
  36. // 3、设置收货人信息
  37. entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
  38. entity.setReceiverProvince(fareResp.getAddress().getProvince());
  39. entity.setReceiverRegion(fareResp.getAddress().getRegion());
  40. entity.setReceiverCity(fareResp.getAddress().getCity());
  41. entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
  42. entity.setReceiverName(fareResp.getAddress().getName());
  43. entity.setReceiverPhone(fareResp.getAddress().getPhone());
  44. // 4、设置订单的相关状态信息
  45. entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
  46. // 5、默认取消信息
  47. entity.setAutoConfirmDay(7);
  48. return entity;
  49. }
  50. /**
  51. * 构建所有订单项数据
  52. * @return
  53. */
  54. private List<OrderItemEntity> buildOrderItems(String orderSn) {
  55. // 最后确定每个购物项的价格
  56. List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
  57. if (currentUserCartItems != null && currentUserCartItems.size()>0){
  58. List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
  59. OrderItemEntity itemEntity = buildOrderItem(cartItem);
  60. itemEntity.setOrderSn(orderSn);
  61. return itemEntity;
  62. }).collect(Collectors.toList());
  63. return itemEntities;
  64. }
  65. return null;
  66. }
  67. /**
  68. * 构建某一个订单项
  69. * @param cartItem
  70. * @return
  71. */
  72. private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
  73. OrderItemEntity itemEntity = new OrderItemEntity();
  74. // 1、订单信息:订单号 v
  75. // 2、商品的spu信息
  76. Long skuId = cartItem.getSkuId();
  77. R r = productFeignService.getSpuInfoBySkuId(skuId);
  78. SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
  79. });
  80. itemEntity.setSpuId(data.getId());
  81. itemEntity.setSpuBrand(data.getBrandId().toString());
  82. itemEntity.setSpuName(data.getSpuName());
  83. itemEntity.setCategoryId(data.getCatalogId());
  84. // 3、商品的sku信息 v
  85. itemEntity.setSkuId(cartItem.getSkuId());
  86. itemEntity.setSkuName(cartItem.getTitle());
  87. itemEntity.setSkuPic(cartItem.getImage());
  88. itemEntity.setSkuPrice(cartItem.getPrice());
  89. itemEntity.setSkuQuantity(cartItem.getCount());
  90. itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
  91. // 4、优惠信息【不做】
  92. // 5、积分信息
  93. itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
  94. itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
  95. // 6、订单项的价格信息
  96. itemEntity.setPromotionAmount(new BigDecimal("0"));
  97. itemEntity.setCouponAmount(new BigDecimal("0"));
  98. itemEntity.setIntegrationAmount(new BigDecimal("0"));
  99. // 当前订单项的实际金额 总额-各种优惠
  100. BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
  101. BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
  102. subtract(itemEntity.getCouponAmount()).
  103. subtract(itemEntity.getIntegrationAmount());
  104. itemEntity.setRealAmount(subtract);
  105. return itemEntity;
  106. }
  107. /**
  108. * 计算价格
  109. * @param orderEntity
  110. * @param itemEntities
  111. */
  112. private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
  113. BigDecimal total = new BigDecimal("0.0");
  114. BigDecimal coupon = new BigDecimal("0.0");
  115. BigDecimal integration = new BigDecimal("0.0");
  116. BigDecimal promotion = new BigDecimal("0.0");
  117. BigDecimal gift = new BigDecimal("0.0");
  118. BigDecimal growth = new BigDecimal("0.0");
  119. // 1、订单的总额,叠加每一个订单项的总额信息
  120. for (OrderItemEntity entity : itemEntities) {
  121. total = total.add(entity.getRealAmount());
  122. coupon = coupon.add(entity.getCouponAmount());
  123. integration = integration.add(entity.getIntegrationAmount());
  124. promotion = promotion.add(entity.getPromotionAmount());
  125. gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
  126. growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
  127. }
  128. // 订单总额
  129. orderEntity.setTotalAmount(total);
  130. // 应付总额
  131. orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
  132. orderEntity.setCouponAmount(coupon);
  133. orderEntity.setIntegrationAmount(integration);
  134. orderEntity.setPromotionAmount(promotion);
  135. }

8、分布式事务优化

8.1、解决低并发场景的分布式事务

8.1.1 Seata的AT模式

回顾Seata:

SpringCloud基础6——分布式事务,Seata

AT模式: 

阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

  • 根据undo-log恢复数据到更新前

 image-20210724175327511

8.1.2【商品模块】保存spu信息业务设为分布式事务  

需求:AT模式实现低并发场景的分布式事务。

下面以保存商品业务为例:

保存商品业务是低并发,包装最终一致性即可,使用seata的AT模式。

 

因为这个业务里涉及到了远程调用,使用本地事务已经无法实现失败回滚,需要使用Seata分布式事务。

1.导入依赖

  1. <!--seata-->
  2. <dependency>
  3. <groupId>com.alibaba.cloud</groupId>
  4. <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  5. </dependency>

 2.对应数据库导入undo_log表

  1. -- 注意此处0.7.0+ 增加字段 context
  2. CREATE TABLE `undo_log` (
  3. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  4. `branch_id` bigint(20) NOT NULL,
  5. `xid` varchar(100) NOT NULL,
  6. `context` varchar(128) NOT NULL,
  7. `rollback_info` longblob NOT NULL,
  8. `log_status` int(11) NOT NULL,
  9. `log_created` datetime NOT NULL,
  10. `log_modified` datetime NOT NULL,
  11. PRIMARY KEY (`id`),
  12. UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
  13. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 3.修改application.yml文件,将事务模式修改为AT模式即可:

  1. seata:
  2. data-source-proxy-mode: AT # 默认就是AT

4.给发起全局事务的入口方法添加@GlobalTransactional注解:

  1. @GlobalTransactional
  2. @Transactional // 本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚。
  3. @Override
  4. public void saveSpuInfo(SpuSaveVo vo) {
  5. //.....
  6. }

5.配置类注入seata代理数据源

  1. @Configuration
  2. public class DataSourceProxyConfig {
  3. @Bean
  4. @ConfigurationProperties(prefix = "spring.datasource")
  5. public DruidDataSource druidDataSource() {
  6. return new DruidDataSource();
  7. }
  8. @Primary
  9. @Bean
  10. public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
  11. return new DataSourceProxy(druidDataSource);
  12. }
  13. }

6.将file.conf和registry.conf两个配置文件移动到项目resources下:

seata-samples/springcloud-jpa-seata/account-service/src/main/resources at master · seata/seata-samples · GitHub

设置file.conf 的 service.vgroup_mapping 配置必须和spring.application.name一致
yml配置service.vgroup_mapping 的服务名:

8.2、解决低并发场景的分布式事务

8.2.1、可靠消息+最终一致性方案

业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。

业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。

案例:

在商品下单业务的最后要锁定库存,我们设置在锁定库存后发RabbitMQ延迟队列消息,通知锁定库存成功,两分钟后消费消息,根据库存信息查询检查订单是否存在,若不存在代表下订单失败,此时要回滚,也就是解锁库存。

8.2.2、若订单失败,自动解锁库存

具体参考下一篇文章:谷粒商城笔记+踩坑(22)——库存自动解锁。RabbitMQ延迟队列_vincewm的博客-CSDN博客

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/462382
推荐阅读
相关标签
  

闽ICP备14008679号