学习目标

1、能够描述超卖问题

2、理解redission的AtomicLong如何初始化库存

3、实现菜品库存的优化

4、能够描述购物车操作流程【添加购物车、移除购物车】

5、理解购物车操作中订单锁的意义,及对菜品库存使用redission的AtomicLong操作

6、能够描述下单操作流程【订单加锁,合并购物车订单项到可核算订单项】

第一章 点餐平台-点餐

1、库存超卖设置

1.1、什么是超卖

当前菜品库存只有1个,当有两个线程过来后,都执行成功了,生成了两个订单,这就是超卖,如图所示:

image.png

方案一:乐观锁机制

使用mybatis plus 里面的@version注解:

  1. @Version
  2. private Integer version;
  • 取出记录时,获取当前 version select version from tab_dish===>thead-A:1,thead-B:2
  • 更新时,带上这个 version
    update tab_dish d set d.dish_num =d.dish_num-1 ,version = 3 where version = 1 and d.dish_num>=0 ,
    update tab_dish d set d.dish_num =d.dish_num-1 ,version = 3 where version = 2 and d.dish_num>=0 ,
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果 version 不对,就更新失败

方案二:比较替换

  • 先查询当前库存数量

thead-A:select d.dish_num from tab_dish where id =’10101’===》old_dish_num=1

thead-B:select d.dish_num from tab_dish where id =’10101’===》old_dish_num=1

  • 执行对应的修改

thead-A:update tab_dish d set d.dish_num =d.dish_num-1 where d.dish_num = #{old_dish_num} and d.dish_num-1>=0 and id =’10101’

thead-B:update tab_dish d set d.dish_num =d.dish_num-1 where d.dish_num = #{old_dish_num} and d.dish_num-1>=0 and id =’10101’

方案三:加lock锁处理

使用redisson分布式锁给当前业务值加锁,保证同一时间只有一个线程执行当前的方法

———-加锁———————

thead-A:select d.dish_num from tab_dish where id =’10101’===》1

thead-A:判断当前库存1>0

thead-A:update tab_dish d set d.dish_num =d.dish_num-1 where id =’10101’

———-解锁———————

方案四:加lock锁,使用atomicLong的方式处理库存

使用reis的单线程特点解决库存的超卖问题

1.2、Redis初始化库存

解决超卖问题关键就是库存的控制,前面我们在讲解redisson的时候,讲解过AtomicLong【原子整长型】,它有如下方法:

  1. `//获得原子性整长形
  2. RAtomicLong atomicLongOper=redissonClient.getAtomicLong("AtomicLongOper");
  3. //添加库存:在餐掌柜项目中:1、项目启动时mysql->redis 2、对菜品进行增删修时改动redis中的库存
  4. atomicLongOper.set(dishNum);
  5. //先递减1,然后返回元素:购物车添加菜品
  6. flag=atomicLongOper.decrementAndGet();
  7. log.info("先递减1,然后返回元素:{}",flag);
  8. //先递增1,然后返回元素:购物车取消菜品
  9. flag=atomicLongOper.incrementAndGet();
  10. log.info("先递增1,然后返回元素:{}",flag);

AtomicLong操作是原子性的,我们可以使用它来初始化、减库存、增库存等操作,我们先看下Redis初始化流程

image.png

1.3、优化-菜品功能

找到DishFaceImpl类做如下优化:

  1. package com.itheima.restkeeper.face;
  2. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  3. import com.itheima.restkeeper.AffixFace;
  4. import com.itheima.restkeeper.DishFace;
  5. import com.itheima.restkeeper.constant.AppletCacheConstant;
  6. import com.itheima.restkeeper.enums.DishEnum;
  7. import com.itheima.restkeeper.exception.ProjectException;
  8. import com.itheima.restkeeper.pojo.Dish;
  9. import com.itheima.restkeeper.pojo.DishFlavor;
  10. import com.itheima.restkeeper.req.AffixVo;
  11. import com.itheima.restkeeper.req.DishVo;
  12. import com.itheima.restkeeper.service.IDishFlavorService;
  13. import com.itheima.restkeeper.service.IDishService;
  14. import com.itheima.restkeeper.utils.BeanConv;
  15. import com.itheima.restkeeper.utils.EmptyUtil;
  16. import com.itheima.restkeeper.utils.ExceptionsUtil;
  17. import lombok.extern.slf4j.Slf4j;
  18. import org.apache.dubbo.config.annotation.DubboReference;
  19. import org.apache.dubbo.config.annotation.DubboService;
  20. import org.apache.dubbo.config.annotation.Method;
  21. import org.redisson.api.RAtomicLong;
  22. import org.redisson.api.RLock;
  23. import org.redisson.api.RedissonClient;
  24. import org.springframework.beans.factory.annotation.Autowired;
  25. import org.springframework.transaction.annotation.Transactional;
  26. import java.util.ArrayList;
  27. import java.util.List;
  28. import java.util.concurrent.TimeUnit;
  29. import java.util.stream.Collectors;
  30. /**
  31. * @ClassName DishFaceImpl.java
  32. * @Description 菜品接口实现
  33. */
  34. @Slf4j
  35. @DubboService(version = "${dubbo.application.version}",timeout = 5000,
  36. methods ={
  37. @Method(name = "findDishVoPage",retries = 2),
  38. @Method(name = "createDish",retries = 0),
  39. @Method(name = "updateDish",retries = 0),
  40. @Method(name = "deleteDish",retries = 0)
  41. })
  42. public class DishFaceImpl implements DishFace {
  43. @Autowired
  44. IDishService dishService;
  45. @Autowired
  46. IDishFlavorService dishFlavorService;
  47. @Autowired
  48. RedissonClient redissonClient;
  49. @DubboReference(version = "${dubbo.application.version}",check = false)
  50. AffixFace affixFace;
  51. @Override
  52. public Page<DishVo> findDishVoPage(DishVo dishVo,
  53. int pageNum,
  54. int pageSize)throws ProjectException {
  55. try {
  56. //查询菜品分页
  57. Page<Dish> page = dishService.findDishVoPage(dishVo, pageNum, pageSize);
  58. Page<DishVo> pageVo = new Page<>();
  59. BeanConv.toBean(page,pageVo);
  60. //结果集转换
  61. List<Dish> dishList = page.getRecords();
  62. List<DishVo> dishVoList = BeanConv.toBeanList(dishList,DishVo.class);
  63. if (!EmptyUtil.isNullOrEmpty(dishVoList)){
  64. dishVoList.forEach(n->{
  65. //处理附件
  66. List<AffixVo> affixVoList = affixFace.findAffixVoByBusinessId(n.getId());
  67. if (!EmptyUtil.isNullOrEmpty(affixVoList)){
  68. n.setAffixVo(affixVoList.get(0));
  69. }
  70. //处理口味1
  71. List<DishFlavor> dishFlavors = dishFlavorService.findDishFlavorByDishId(n.getId());
  72. List<String> dishFavorList = new ArrayList<>();
  73. for (DishFlavor dishFlavor : dishFlavors) {
  74. dishFavorList.add(String.valueOf(dishFlavor.getDataKey()));
  75. }
  76. String[] dishFlavorDataKey = new String[dishFavorList.size()];
  77. dishFavorList.toArray(dishFlavorDataKey);
  78. n.setHasDishFlavor(dishFlavorDataKey);
  79. });
  80. }
  81. pageVo.setRecords(dishVoList);
  82. //返回结果
  83. return pageVo;
  84. } catch (Exception e) {
  85. log.error("查询菜品列表异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  86. throw new ProjectException(DishEnum.PAGE_FAIL);
  87. }
  88. }
  89. @Override
  90. @Transactional
  91. public DishVo createDish(DishVo dishVo) throws ProjectException{
  92. try {
  93. //创建菜品
  94. DishVo dishVoResult = BeanConv.toBean(dishService.createDish(dishVo), DishVo.class);
  95. dishVoResult.setHasDishFlavor(dishVo.getHasDishFlavor());
  96. //绑定附件
  97. if (!EmptyUtil.isNullOrEmpty(dishVoResult)){
  98. affixFace.bindBusinessId(AffixVo.builder()
  99. .businessId(dishVoResult.getId())
  100. .id(dishVo.getAffixVo().getId())
  101. .build());
  102. }
  103. dishVoResult.setAffixVo(AffixVo.builder()
  104. .pathUrl(dishVo.getAffixVo().getPathUrl())
  105. .businessId(dishVoResult.getId())
  106. .id(dishVo.getAffixVo().getId()).build());
  107. //构建初始化库存
  108. String key = AppletCacheConstant.REPERTORY_DISH+dishVoResult.getId();
  109. RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
  110. atomicLong.set(dishVoResult.getDishNumber());
  111. return dishVoResult;
  112. } catch (Exception e) {
  113. log.error("保存菜品异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  114. throw new ProjectException(DishEnum.CREATE_FAIL);
  115. }
  116. }
  117. @Override
  118. @Transactional
  119. public Boolean updateDish(DishVo dishVo) throws ProjectException {
  120. Boolean flag = false;
  121. //修改菜品
  122. flag = dishService.updateDish(dishVo);
  123. //处理菜品图片
  124. if (flag){
  125. List<AffixVo> affixVoList = affixFace.findAffixVoByBusinessId(dishVo.getId());
  126. List<Long> affixIds = affixVoList.stream()
  127. .map(AffixVo::getId)
  128. .collect(Collectors.toList());
  129. if (!affixIds.contains(dishVo.getAffixVo().getId())){
  130. //删除图片
  131. flag = affixFace.deleteAffixVoByBusinessId(dishVo.getId());
  132. //绑定新图片
  133. affixFace.bindBusinessId(AffixVo.builder()
  134. .businessId(dishVo.getId())
  135. .id(dishVo.getAffixVo().getId())
  136. .build());
  137. }
  138. }
  139. //构建redis库存
  140. String key = AppletCacheConstant.REPERTORY_DISH+dishVo.getId();
  141. RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
  142. atomicLong.set(dishVo.getDishNumber());
  143. return flag;
  144. }
  145. @Override
  146. @Transactional
  147. public Boolean deleteDish(String[] checkedIds)throws ProjectException {
  148. try {
  149. Boolean flag = dishService.deleteDish(checkedIds);
  150. for (String checkedId : checkedIds) {
  151. //删除图片
  152. affixFace.deleteAffixVoByBusinessId(Long.valueOf(checkedId));
  153. //删除菜品库存
  154. String key = AppletCacheConstant.REPERTORY_DISH+checkedId;
  155. RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
  156. atomicLong.delete();
  157. }
  158. return flag;
  159. } catch (Exception e) {
  160. log.error("删除菜品异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  161. throw new ProjectException(DishEnum.DELETE_FAIL);
  162. }
  163. }
  164. @Override
  165. public DishVo findDishByDishId(Long dishId)throws ProjectException {
  166. try {
  167. //按菜品ID查找菜品
  168. Dish dish = dishService.getById(dishId);
  169. if (!EmptyUtil.isNullOrEmpty(dish)){
  170. return BeanConv.toBean(dish,DishVo.class);
  171. }
  172. return null;
  173. } catch (Exception e) {
  174. log.error("查找菜品所有菜品异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  175. throw new ProjectException(DishEnum.SELECT_DISH_FAIL);
  176. }
  177. }
  178. }

项目启动时InitDish类自动初始化所有起售且有效状态的菜品库存

  1. package com.itheima.restkeeper.init;
  2. import com.itheima.restkeeper.constant.AppletCacheConstant;
  3. import com.itheima.restkeeper.pojo.Dish;
  4. import com.itheima.restkeeper.service.IDishService;
  5. import org.redisson.api.RAtomicLong;
  6. import org.redisson.api.RedissonClient;
  7. import org.redisson.client.RedisClient;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Component;
  10. import javax.annotation.PostConstruct;
  11. import java.util.List;
  12. /**
  13. * @ClassName InitDish.java
  14. * @Description 初始化菜品库存
  15. */
  16. @Component
  17. public class InitDish {
  18. @Autowired
  19. IDishService dishService;
  20. @Autowired
  21. RedissonClient redissonClient;
  22. @PostConstruct
  23. public void initDishNumber(){
  24. //查询所有有效的且起售状态的菜品
  25. List<Dish> dishList = dishService.findDishVos();
  26. for (Dish dish : dishList) {
  27. //构建初始化库存
  28. String key = AppletCacheConstant.REPERTORY_DISH+dish.getId();
  29. RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
  30. atomicLong.set(dish.getDishNumber());
  31. }
  32. }
  33. }

2、购物车操作

2.1、功能区拆解

开桌之后用户可以在H5首页选购菜品,及菜品的数量,如图所示:

image.png

下图为整个购物车操作的流程图,我们首先进行下流程分析:

image.png

2.2、数据结构

购物车:在Redis中临时存储选购的菜品、选购的数量

其中数据结构就是OrderItemVo的json字符串,其结构如下:

image.png

2.3、功能开发

image.png

  • AppletController
    传入菜品ID、订单编号、菜品口味、操作动作请求opertionShoppingCart方法

    1. @PostMapping("opertion-shopping-cart/{dishId}/{orderNo}/{dishFlavor}/{opertionType}")
    2. @ApiOperation(value = "操作购物车",notes = "每次点击添加或减少1,则添加或减少购物车订单项1,同时增减库存1")
    3. @ApiImplicitParams({
    4. @ApiImplicitParam(paramType = "path",name = "dishId",value = "菜品ID",dataType = "Long"),
    5. @ApiImplicitParam(paramType = "path",name = "orderNo",value = "订单编号",dataType = "Long"),
    6. @ApiImplicitParam(paramType = "path",name = "dishFlavor",value = "菜品口味",dataType = "String"),
    7. @ApiImplicitParam(paramType = "path",name = "opertionType",value = "操作动作",dataType = "String")
    8. })
    9. public ResponseWrap<OrderVo> opertionShoppingCart(
    10. @PathVariable("dishId") Long dishId,
    11. @PathVariable("orderNo") Long orderNo,
    12. @PathVariable("dishFlavor") String dishFlavor,
    13. @PathVariable("opertionType") String opertionType) {
    14. OrderVo orderVoResult = appletFace.opertionShoppingCart(dishId,orderNo,dishFlavor,opertionType);
    15. return ResponseWrapBuild.build(BrandEnum.SUCCEED,orderVoResult);
    16. }
  • AppletFace

    1. /***
    2. * @description 每次点击添加或减少,则添加或减少购物车订单项,同时增减库存
    3. * 【1】加入购物车逻辑:
    4. * 首先进行库存判定,如果库存够,则减库存,新增购物车订单项,如果库存不够则给出相对应提示信息,且回滚库存
    5. * 【2】减少购物车逻辑:
    6. * 首先从购物车订单项中移除菜品信息,回滚库存
    7. * @param dishId 菜品ID
    8. * @param orderNo 订单Id
    9. * @param dishFlavor 口味
    10. * @param opertionType 操作方式:移除购物车:REMOVE,加入购物车:ADD
    11. * @return
    12. */
    13. OrderVo opertionShoppingCart(Long dishId,
    14. Long orderNo,
    15. String dishFlavor,
    16. String opertionType) throws ProjectException;
  • AppletFaceImpl
    购物车功能-总述
    image.png
    购物车功能-添加

    1、获得订单加锁

    2、是否获得到锁

    3、操作购物车订单项

    1. 3.1、添加到购物车订单项
    2. 3.2、移除购物车订单项

    4、查询当前订单信息,并且处理订单项

  1. @Override
  2. @Transactional
  3. public OrderVo opertionShoppingCart(Long dishId,
  4. Long orderNo,
  5. String dishFlavor,
  6. String opertionType) throws ProjectException {
  7. //1、获得订单加锁:此处多余:因为此业务中使用atomicLong本身就是原子性的,所以不需要加锁
  8. String keyOrder = AppletCacheConstant.ADD_TO_ORDERITEM_LOCK + orderNo;
  9. RLock lockOrder = redissonClient.getLock(keyOrder);
  10. OrderVo orderVoResult = null;
  11. try {
  12. //2、因为此业务中使用atomicLong本身就是原子性的,所以不需要加锁
  13. if (lockOrder.tryLock(
  14. AppletCacheConstant.REDIS_WAIT_TIME,
  15. AppletCacheConstant.REDIS_LEASETIME,
  16. TimeUnit.SECONDS)) {
  17. String keyDish = AppletCacheConstant.REPERTORY_DISH + dishId;
  18. RAtomicLong atomicLong = redissonClient.getAtomicLong(keyDish);
  19. //3.1、添加到购物车订单项
  20. if (opertionType.equals(SuperConstant.OPERTION_TYPE_ADD)) {
  21. this.addToShoppingCart(dishId, orderNo,dishFlavor, atomicLong);
  22. }
  23. //3.2、移除购物车订单项
  24. if (opertionType.equals(SuperConstant.OPERTION_TYPE_REMOVE)) {
  25. this.removeToShoppingCart(dishId, orderNo, atomicLong);
  26. }
  27. //4、查询当前订单信息,并且处理订单项
  28. orderVoResult = orderService.findOrderByOrderNo(orderNo);
  29. return handlerOrderVo(orderVoResult);
  30. }
  31. } catch (InterruptedException e) {
  32. log.error("===编辑dishId:{},orderNo:{}进入购物车加锁失败:{}",
  33. dishId, orderNo, ExceptionsUtil.getStackTraceAsString(e));
  34. throw new ProjectException(OpenTableEnum.TRY_LOCK_FAIL);
  35. } finally {
  36. lockOrder.unlock();
  37. }
  38. return orderVoResult;
  39. }

image.png

6

  1. /**
  2. * @param dishId 菜品ID
  3. * @param orderNo 订单编号
  4. * @param atomicLong 原子计数器
  5. * @return
  6. * @description 添加购物车订单项
  7. */
  8. private void addToShoppingCart(Long dishId,
  9. Long orderNo,
  10. String dishFlavor,
  11. RAtomicLong atomicLong) {
  12. //1、如果库存够,redis减库存
  13. if (atomicLong.decrementAndGet() >= 0) {
  14. //2、mysql菜品表库存
  15. Boolean flag = dishService.updateDishNumber(-1L, dishId);
  16. if (!flag) {
  17. //减菜品库存失败,归还redis菜品库存
  18. atomicLong.incrementAndGet();
  19. throw new ProjectException(ShoppingCartEnum.UPDATE_DISHNUMBER_FAIL);
  20. }
  21. //3、查询redis缓存的购物车订单项
  22. String key = AppletCacheConstant.ORDERITEMVO_STATISTICS + orderNo;
  23. RMapCache<Long, OrderItemVo> orderItemVoRMap = redissonClient.getMapCache(key);
  24. OrderItemVo orderItemHandler = orderItemVoRMap.get(dishId);
  25. //4.1、如果以往购物车订单项中无则新增
  26. if (EmptyUtil.isNullOrEmpty(orderItemHandler)) {
  27. Dish dish = dishService.getById(dishId);
  28. List<AffixVo> affixVoList = affixFace.findAffixVoByBusinessId(dish.getId());
  29. OrderVo orderVo = orderService.findOrderByOrderNo(orderNo);
  30. OrderItemVo orderItemVo = OrderItemVo.builder()
  31. .productOrderNo(orderNo)
  32. .categoryId(dish.getCategoryId())
  33. .dishId(dishId)
  34. .dishName(dish.getDishName())
  35. .dishFlavor(dishFlavor)
  36. .dishNum(1L)
  37. .price(dish.getPrice())
  38. .reducePrice(dish.getReducePrice())
  39. .build();
  40. orderItemVo.setAffixVo(affixVoList.get(0));
  41. //沿用订单中的分库键
  42. orderItemVo.setShardingId(orderVo.getShardingId());
  43. orderItemVoRMap.put(dishId, orderItemVo);
  44. //4.2、如果以往购物车订单项中有此菜品,则进行购物车订单项数量递增
  45. } else {
  46. orderItemHandler.setDishNum(orderItemHandler.getDishNum() + 1);
  47. orderItemVoRMap.put(dishId, orderItemHandler);
  48. }
  49. } else {
  50. //5、redis库存不足,虽然可以不处理,但建议还是做归还库存
  51. atomicLong.incrementAndGet();
  52. throw new ProjectException(ShoppingCartEnum.UNDERSTOCK);
  53. }
  54. }

购物车功能-移除

image.png

1、菜品库存增加

2、查询redis缓存的购物车订单项

3、购物车订单项存在

  1. 3.1、购物车订单项的数量大于1,修改当前数量
  2. 3.2购物车订单项的数量等于1,则删除此订单项

4、redis菜品库存增加

  1. /**
  2. * @param dishId 菜品ID
  3. * @param orderNo 订单编号
  4. * @param atomicLong 原子计数器
  5. * @return
  6. * @description 移除购物车订单项
  7. */
  8. private void removeToShoppingCart(Long dishId, Long orderNo, RAtomicLong atomicLong) {
  9. boolean flag = true;
  10. //1、菜品库存增加
  11. flag = dishService.updateDishNumber(1L, dishId);
  12. if (!flag) {
  13. throw new ProjectException(ShoppingCartEnum.UPDATE_DISHNUMBER_FAIL);
  14. }
  15. //2、查询redis缓存的购物车订单项
  16. String key = AppletCacheConstant.ORDERITEMVO_STATISTICS + orderNo;
  17. RMapCache<Long, OrderItemVo> orderItemVoRMap = redissonClient.getMapCache(key);
  18. OrderItemVo orderItemHandler = orderItemVoRMap.get(dishId);
  19. //3、购物车订单项存在
  20. if (!EmptyUtil.isNullOrEmpty(orderItemHandler)) {
  21. //3.1、购物车订单项的数量大于1,修改当前数量
  22. if (orderItemHandler.getDishNum().intValue() > 1) {
  23. orderItemHandler.setDishNum(orderItemHandler.getDishNum() - 1);
  24. orderItemVoRMap.put(dishId, orderItemHandler);
  25. } else {
  26. //3.2购物车订单项的数量等于1,则删除此订单项
  27. orderItemVoRMap.remove(dishId, orderItemHandler);
  28. }
  29. //4、redis菜品库存增加
  30. atomicLong.incrementAndGet();
  31. }else {
  32. throw new ProjectException(ShoppingCartEnum.UPDATE_DISHNUMBER_FAIL);
  33. }
  34. }

第二章 点餐平台-下订单

1、功能区拆解

image.png

用户在选择好菜品及数量后,点击去结算按钮,则会把购物车的订单项菜品同步到可核算订单项中,操作如下:

image.png

2、功能开发

  • AppletController
    传入当前要进行下单的订单号orderNo
  1. @PostMapping("placeOrder/{orderNo}")
  2. @ApiOperation(value = "下单操作",notes = "添加购物车订单项到可结算订单项")
  3. @ApiImplicitParam(paramType = "path",name = "orderNo",value = "订单编号",dataType = "Long")
  4. public ResponseWrap<OrderVo> placeOrder(@PathVariable("orderNo") Long orderNo) {
  5. OrderVo orderVoResult = appletFace.placeOrder(orderNo);
  6. return ResponseWrapBuild.build(BrandEnum.SUCCEED,orderVoResult);
  7. }
  • AppletFace
  1. /***
  2. * @description 下单操作:添加购物车订单项到可结算订单项
  3. * @param orderNo 订单编号
  4. * @return
  5. */
  6. OrderVo placeOrder(Long orderNo) throws ProjectException;
  • AppletFaceImpl

image.png

1、锁定订单

2、查询可以核算订单项

3、查询购物车订单项

4、购物车订单项不为空才合并

5、求交集:购物车订单项与核算订单项合并

6、求差集:保存购物车订单项到可核算订单项

7、计算更新订单金额

9、清理redis购物车订单项目

9、再次查询订单

  1. @Override
  2. @Transactional
  3. public OrderVo placeOrder(Long orderNo) throws ProjectException {
  4. try {
  5. //1、锁定订单
  6. String key = AppletCacheConstant.ADD_TO_ORDERITEM_LOCK + orderNo;
  7. RLock lock = redissonClient.getLock(key);
  8. OrderVo orderVoResult = null;
  9. try {
  10. if (lock.tryLock(
  11. AppletCacheConstant.REDIS_WAIT_TIME,
  12. AppletCacheConstant.REDIS_LEASETIME,
  13. TimeUnit.SECONDS)) {
  14. Boolean flag = true;
  15. orderVoResult = orderService.findOrderByOrderNo(orderNo);
  16. //2、查询可以核算订单项
  17. List<OrderItem> orderItemList = orderItemService
  18. .findOrderItemByOrderNo(orderVoResult.getOrderNo());
  19. List<OrderItemVo> orderItemVoStatisticsList = BeanConv
  20. .toBeanList(orderItemList, OrderItemVo.class);
  21. if (EmptyUtil.isNullOrEmpty(orderItemVoStatisticsList)) {
  22. orderItemVoStatisticsList = new ArrayList<>();
  23. }
  24. //3、查询购物车订单项
  25. key = AppletCacheConstant.ORDERITEMVO_STATISTICS + orderNo;
  26. RMapCache<Long, OrderItemVo> orderItemVoRMap = redissonClient.getMapCache(key);
  27. List<OrderItemVo> orderItemVoTemporaryList = (List<OrderItemVo>)orderItemVoRMap
  28. .readAllValues();
  29. //4、购物车订单项不为空才合并
  30. if (!EmptyUtil.isNullOrEmpty(orderItemVoTemporaryList)) {
  31. //5、求交集:购物车订单项与核算订单项合并
  32. flag = this.intersection(flag,orderItemVoStatisticsList,orderItemVoTemporaryList);
  33. if (!flag) {
  34. throw new ProjectException(OrderItemEnum.UPDATE_ORDERITEM_FAIL);
  35. }
  36. //6、求差集:保存购物车订单项到可核算订单项
  37. flag = this.difference(flag,orderItemVoStatisticsList,orderItemVoTemporaryList);
  38. if (!flag) {
  39. throw new ProjectException(OrderItemEnum.UPDATE_ORDERITEM_FAIL);
  40. }
  41. //7、计算更新订单信息
  42. flag = this.calculateOrderAmount(orderNo,orderVoResult);
  43. if (!flag) {
  44. throw new ProjectException(OrderItemEnum.SAVE_ORDER_FAIL);
  45. }
  46. //8、清理redis购物车订单项目
  47. orderItemVoTemporaryList.forEach(n->{
  48. orderItemVoRMap.remove(n.getDishId());
  49. });
  50. }
  51. }
  52. } catch (InterruptedException e) {
  53. log.error("合并订单出错:{}",ExceptionsUtil.getStackTraceAsString(e));
  54. throw new ProjectException(OrderItemEnum.LOCK_ORDER_FAIL);
  55. } finally {
  56. lock.unlock();
  57. }
  58. //9、再次查询订单
  59. return handlerOrderVo(orderVoResult);
  60. }catch (Exception e){
  61. log.error("下单操作异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  62. throw new ProjectException(OrderEnum.PLACE_ORDER_FAIL);
  63. }
  64. }
  1. /***
  2. * @description 求交集:购物车订单项与核算订单项合并
  3. * @param flag 是否操作成功
  4. * @param orderItemVoStatisticsList 可核算订单项
  5. * @param orderItemVoTemporaryList 购物车订单项
  6. * @return 是否操作成功
  7. */
  8. private Boolean intersection(boolean flag,
  9. List<OrderItemVo> orderItemVoStatisticsList,
  10. List<OrderItemVo> orderItemVoTemporaryList){
  11. //1、求交集
  12. List<OrderItemVo> listIntersection = new ArrayList<>();
  13. listIntersection.addAll(orderItemVoTemporaryList);
  14. listIntersection.retainAll(orderItemVoStatisticsList);
  15. if (!EmptyUtil.isNullOrEmpty(listIntersection)) {
  16. //2、获得核算订单项中需要增加的订单项
  17. List<OrderItemVo> orderItemVoHandler = orderItemVoStatisticsList
  18. .stream().filter(n ->
  19. listIntersection.contains(n)).collect(Collectors.toList());
  20. List<OrderItem> orderItemHandler = BeanConv
  21. .toBeanList(orderItemVoHandler, OrderItem.class);
  22. //3、循环累加当前核算订单的菜品数量
  23. List<OrderItem> orderItems = new ArrayList<>();
  24. orderItemHandler.forEach(n -> {
  25. listIntersection.forEach(k -> {
  26. if (n.getDishId().longValue() == k.getDishId().longValue()) {
  27. OrderItem orderItem = OrderItem.builder()
  28. .id(n.getId())
  29. .dishNum(n.getDishNum() + k.getDishNum()).build();
  30. orderItems.add(orderItem);
  31. }
  32. });
  33. });
  34. //4、批量修改可结算订单项目
  35. flag = orderItemService.updateBatchById(orderItems);
  36. }
  37. return flag;
  38. }
  1. /***
  2. * @description 求差集:保存购物车订单项到可核算订单项
  3. * @param flag 是否操作成功
  4. * @param orderItemVoStatisticsList 可核算订单项
  5. * @param orderItemVoTemporaryList 购物车订单项
  6. * @return 是否操作成功
  7. */
  8. private Boolean difference(boolean flag,
  9. List<OrderItemVo> orderItemVoStatisticsList,
  10. List<OrderItemVo> orderItemVoTemporaryList){
  11. //1、求差集
  12. List<OrderItemVo> listDifference = new ArrayList<>();
  13. listDifference.addAll(orderItemVoTemporaryList);
  14. listDifference.removeAll(orderItemVoStatisticsList);
  15. //2、处理订单项
  16. if (!EmptyUtil.isNullOrEmpty(listDifference)) {
  17. List<OrderItem> orderItems = BeanConv
  18. .toBeanList(listDifference, OrderItem.class);
  19. flag = orderItemService.saveBatch(orderItems);
  20. }
  21. return flag;
  22. }
  1. /***
  2. * @description 计算更新订单信息
  3. * @param orderNo 订单编号
  4. * @param orderVoResult 计算结果
  5. * @return
  6. * @return: java.lang.Boolean
  7. */
  8. private Boolean calculateOrderAmount(Long orderNo,OrderVo orderVoResult){
  9. //1、计算订单金额
  10. List<OrderItem> orderItemListResult = orderItemService
  11. .findOrderItemByOrderNo(orderNo);
  12. BigDecimal sumPrice = orderItemListResult.stream()
  13. .map(n -> {
  14. BigDecimal price = n.getPrice();
  15. BigDecimal reducePrice = n.getReducePrice();
  16. Long dishNum = n.getDishNum();
  17. //如果有优惠价格以优惠价格计算
  18. if (EmptyUtil.isNullOrEmpty(reducePrice)) {
  19. return price.multiply(new BigDecimal(dishNum));
  20. } else {
  21. return reducePrice.multiply(new BigDecimal(dishNum));
  22. }
  23. }
  24. ).reduce(BigDecimal.ZERO, BigDecimal::add);
  25. //2、更新订单金额信息
  26. orderVoResult.setPayableAmountSum(sumPrice);
  27. OrderVo orderVoHandler = OrderVo.builder()
  28. .id(orderVoResult.getId())
  29. .payableAmountSum(sumPrice)
  30. .build();
  31. return orderService.updateById(BeanConv.toBean(orderVoHandler, Order.class));
  32. }

课堂讨论

1、什么是库存超卖?餐掌柜中是如何解决超卖问题的?

2、除了使用atomicLong来处理库存,你还有什么样的解决方案?

3、简述添加购物车的流程?

4、多人同时点餐,你是怎么做到让大家看到的信息都是同步的?

5、点餐后不下单,你如何同步归还库存,有哪些方案?

6、简述下订单的流程【如何合并购物车订单项到可核算订单项】?

7、为什么你们的项目在操作购物车的时候就扣库存呢?有没有更好的方案?

课后任务

1、完成点餐、下订单开发,同步到git【☆☆☆☆☆】

2、完成当天课堂讨论,同步到git【☆☆☆☆☆】

3、完成2-5道sql练习【☆☆☆☆】

4、梳理项目二-点餐、下订单业务,同步到git【☆☆☆】