ToB企服应用市场:ToB评测及商务社交产业平台

标题: SpringCloud(十一)- 秒杀 抢购 [打印本页]

作者: 农民    时间: 2022-11-25 16:04
标题: SpringCloud(十一)- 秒杀 抢购
1、流程图

1.1 数据预热


1.2 抢购


1.3 生成订单 (发送订单消息)


1.4 订单入库 (监听 消费订单消息)


1.5 查看订单状态


1.6 支付 (获取支付链接 )


1.7 支付成功 微信回调 (发送 支付成功消息)


1.8  支付成功 返回给前端成功 (监听 支付成功消息)


2、incr 和 setnx

2.1 incr

Redis Incr 命令将 key 中储存的数字值增一。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。且将key的有效时间设置为长期有效 。
2.1.1 常见使用场景

2.1.1.1 计数

我们可能常会统计网站页面每天访问量,通过incr命令在redis中设置key,每次增加1,设置24小时过期。
2.1.1.2 限流

日常的开放平台API一般常有限流,利用redis的incr命令可以实现一般的限流操作。如限制某接口每分钟请求次数上限1000次
2.1.1.3 幂等

MQ防止重复消费也可以利用INCR命令实现,如订单防重,订单5分钟之内只能被消费一次,订单号作为redis的key
2.2 sexnx

Redis使用setnx命令实现分布式锁;
2.1.1 加锁
  1. 版本一#但是这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败。
  2. setnx key value
  3. 版本二#给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。
  4. setnx key value
  5. expire key seconds
  6. 版本三(推荐)#通过“set...nx...”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。
  7. set key value nx ex seconds
复制代码
2.1.2 解锁

解锁就是删除代表锁的那份数据。
  1. del key
复制代码
参考博客:https://blog.csdn.net/weixin_45525272/article/details/126562119

3、实现代码

主要是抢购的业务;
3.1模块分析


3.2 web模块


3.2.1 application.yml

点击查看代码
  1. # 端口
  2. server:
  3.   port: 8106
  4. # 服务名
  5. spring:
  6.   application:
  7.     name: edocmall-seckill-web
  8. # redis 配置
  9.   redis:
  10.     host: 127.0.0.1
  11.     port: 6379
  12. # eureka 注册中心的配置
  13. eureka:
  14.   client:
  15.     service-url:
  16.       defaultZone: http://127.0.0.1:8096/eureka  # 注册中心的地址
  17. # 秒杀抢购自定义配置
  18. kh96:
  19.   seckill:
  20.     buy-user-count-prefix: seckill-buy-user-count # 请求用户数量的限制头标识
  21.     buy-prod-stock-count: 100 # 初始化腔骨商品的库存数量,存入redis,实际开发中,没有此配置(初始化商品库存,在洗头膏添加抢购商品是)
  22.     buy-prod-stock-prefix: seckill-buy-prod-stock # 抢购商品数量 头标识
  23.     buy-user-lock-prefix: seckill-buy-user-lock # 锁定抢购用户的头标识
  24.     buy-prod-lock-prefix: seckill-buy-prod-lock # 锁定商品库存锁头标识
复制代码
3.2.2 SeckillConfig 配置类

点击查看代码
  1. /**
  2. * Created On : 9/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: SeckillConfig
  7. */
  8. @Data
  9. @Component
  10. @ConfigurationProperties(prefix = "kh96.seckill")
  11. public class SeckillConfig {
  12.     /*
  13.         请求用户数量的限制头标识
  14.      */
  15.     private String  buyUserCountPrefix;
  16.     /*
  17.         初始化抢购商品的库存数量
  18.      */
  19.     private Integer buyProdStockCount;
  20.     /*
  21.         抢购商品数量 头标识
  22.      */
  23.     private String buyProdStockPrefix;
  24.     /*
  25.         锁定抢购用户的头标识
  26.      */
  27.     private String buyUserLockPrefix;
  28.     /*
  29.         锁定商品库存锁头标识
  30.      */
  31.     private String buyProdLockPrefix;
  32. }
复制代码
3.2.3 抢购接口和实现类

3.2.3.1 接口

点击查看代码
  1. /**
  2. * Created On : 9/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: 秒杀抢购的业务接口
  7. */
  8. public interface SeckillService {
  9.     /**
  10.      * @author : huayu
  11.      * @date   : 9/11/2022
  12.      * @param  : [prodId, stockCount]
  13.      * @return : void
  14.      * @description : 初始化商品库存到缓存
  15.      */
  16.     boolean  initProdStock2Redis(String prodId,Integer stockCount);
  17.     /**
  18.      * @author : huayu
  19.      * @date   : 9/11/2022
  20.      * @param  : [productId]
  21.      * @return : boolean
  22.      * @description : 校验抢购商品的请求数量是否达到上限
  23.      */
  24.     boolean checkBuyUserCount(String productId);
  25.     /**
  26.      * @author : huayu
  27.      * @date   : 9/11/2022
  28.      * @param  : [prodId, buyCount]
  29.      * @return : boolean
  30.      * @description : 校验商品库存是否充足
  31.      */
  32.     boolean checkProdStockEnough(String prodId,Integer buyCount);
  33.     /**
  34.      * @author : huayu
  35.      * @date   : 9/11/2022
  36.      * @param  : [userId, prodId]
  37.      * @return : boolean
  38.      * @description : 校验用户是否已经抢购过当前商品
  39.      */
  40.     boolean checkBuyUserBought(String userId,String prodId);
  41.     /**
  42.      * @author : huayu
  43.      * @date   : 9/11/2022
  44.      * @param  : [userId]
  45.      * @return : boolean
  46.      * @description : 校验商品库存是否锁定
  47.      */
  48.     boolean checkProdStockLocked(String prodId);
  49.     /**
  50.      * @author : huayu
  51.      * @date   : 9/11/2022
  52.      * @param  : [prodId]
  53.      * @return : boolean
  54.      * @description : 释放库存锁
  55.      */
  56.     void unLockProdStock(String prodId);
  57.     /**
  58.      * @author : huayu
  59.      * @date   : 9/11/2022
  60.      * @param  : [prodId, butCount]
  61.      * @return : void
  62.      * @description : 扣减库存
  63.      */
  64.     void subProdStockCount(String prodId, Integer butCount);
  65. }
复制代码
3.2.3.2 实现类

点击查看代码
  1. /**
  2. * Created On : 9/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: 抢购业务接口实现类
  7. */
  8. @Service
  9. public class SeckillServiceImpl implements SeckillService {
  10.     @Autowired
  11.     private RedisUtils redisUtils;
  12.     @Autowired
  13.     private SeckillConfig seckillConfig;
  14.     @Override
  15.     public boolean initProdStock2Redis(String prodId, Integer stockCount) {
  16.         // 判断redis中,是否存在已经初始化的商品库存数据,如果已经存在,不需要再次初始化
  17.         if (ObjectUtils.isEmpty(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId))){
  18.             // 将商品库存添加到redis中
  19.             return redisUtils.set(seckillConfig.getBuyProdStockPrefix() + ":" + prodId, stockCount);
  20.         }
  21.         // 已经存在商品库存,不需要设置
  22.         return false;
  23.     }
  24.     @Override
  25.     public boolean checkBuyUserCount(String prodId) {
  26.         // 使用redis的incr操作,校验当前商品的抢购用户数是否达到上限
  27.         return redisUtils.incr(seckillConfig.getBuyUserCountPrefix() + ":" + prodId, 1)
  28.                 > Integer.parseInt(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId).toString()) * 2;
  29.     }
  30.     @Override
  31.     public boolean checkProdStockEnough(String prodId, Integer buyCount) {
  32.         //校验商品库存,是否大于等于用户抢购数量,如果小于,代表库存不足
  33.         //TODO 如果redis 没有查到库存(库存没初始化,或者后台误删,必须要进行数据库查询操作,获取库存,
  34.         // 要加锁,只有一个线程取操作),再同步到redis
  35.         return Integer.parseInt(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId).toString())
  36.                 >= buyCount;
  37.     }
  38.     @Override
  39.     public boolean checkBuyUserBought(String userId, String prodId) {
  40.         //使用redis的分布式锁,sexnx,
  41.         // 如果上锁成功,代表没有买过,可以继续购买,如果上锁失败,表示购买过不能继续购买
  42.         //锁时长为正抢购活动时长
  43.         return ! redisUtils.lock( seckillConfig.getBuyUserLockPrefix()+":"+userId,
  44.                                 null,
  45.                                 15*60); //模拟 抢购时长
  46.     }
  47.     @Override
  48.     public boolean checkProdStockLocked(String prodId) {
  49.         //使用redis的分布式锁,sexnx,如果上锁成功,代表库存没有被锁,如果上锁失败代表库存被其他用户锁定不能购买
  50.         //锁库存必须要增加时长限制,防止库存锁释放失败,导致用户无法抢购,过期仍然会释放
  51.         return ! redisUtils.lock(seckillConfig.getBuyProdLockPrefix()+":"+prodId,
  52.                                 null,
  53.                                 2*60); //模拟锁2分钟
  54.     }
  55.     @Override
  56.     public void unLockProdStock(String prodId) {
  57.         //删除redis中的 库存锁
  58.         redisUtils.del(seckillConfig.getBuyProdLockPrefix()+":"+prodId);
  59.     }
  60.     @Override
  61.     public void subProdStockCount(String prodId, Integer butCount) {
  62.         //扣减 redis中缓存的商品库存数量
  63.         //TODO redis缓存中操成功后,要发异步消息 到队列  更新数据库中商品库存呢
  64.         redisUtils.decr(seckillConfig.getBuyProdStockPrefix() + ":" + prodId,
  65.                 butCount);
  66.     }
  67. }
复制代码
3.2.4 远程调用订单模块进行下单

点击查看代码
  1. /**
  2. * Created On : 10/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: SeckillOrderFeignService
  7. */
  8. @FeignClient(value = "edocmall-seckill-order")
  9. public interface SeckillOrderFeignService {
  10.     /**
  11.      * @author : huayu
  12.      * @date   : 10/11/2022
  13.      * @param  : [userId, prodId, buyCount]
  14.      * @return : java.lang.String
  15.      * @description : 远程调用订单中心,生成秒杀抢购订单的接口
  16.      */
  17.     @GetMapping("/createSeckillOrder")
  18.     String feignInvokeCreateSeckillOrder(@RequestParam(value = "userId") String userId,
  19.                               @RequestParam(value = "prodId") String prodId,
  20.                               @RequestParam(value = "buyCount") Integer buyCount);
  21.    
  22. }
复制代码
3.2.5 抢购 控制层

点击查看代码
  1. /**
  2. * Created On : 9/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: 秒杀操作入口
  7. */
  8. @Slf4j
  9. @RestController
  10. @Api(tags = "秒杀操作")
  11. public class SeckillController {
  12.     @Autowired
  13.     private SeckillService seckillService;
  14.     @Autowired
  15.     private SeckillOrderFeignService seckillOrderFeignService;
  16.     /**
  17.      * @author : zhukang
  18.      * @date   : 2022/11/9
  19.      * @param  : [userId, prodId, buyCount]
  20.      * @return : com.kgc.scd.util.RequestResult<?>
  21.      * @description : 模拟初始化商品库存,实际开发中,没有此操作,是在后台添加抢购商品预热是,直接同步到redis
  22.      */
  23.     @GetMapping("/initProdStock")
  24.     @ApiOperation(value = "1 初始库存", notes = "抢购商品预热,初始化商品库存数量到redis中")
  25.     @ApiImplicitParams({
  26.             @ApiImplicitParam(value = "初始化商品id",name = "prodId"),
  27.             @ApiImplicitParam(value = "初始化商品数量",name = "stockCount")
  28.     })
  29.     public RequestResult<?> initProdStock(@RequestParam String prodId,
  30.                                           @RequestParam Integer stockCount){
  31.         log.info("------ 初始化商品:{},库存数量:{},到redis缓存中 ------", prodId, stockCount);
  32.         // 增加业务逻辑处理:防止瞬时请求过多,使用redis的incr原子操作,进行请求用户数的统计计数,如果请求已经达到了上限(比如:设定请求用户数最多是抢购商品库存的2倍),其它后续所有的请求都默认无效,返回失败:当前请求用户过多,请稍后重试
  33.         if(seckillService.initProdStock2Redis(prodId, stockCount)){
  34.             return ResultBuildUtil.success("初始化商品库存成功!");
  35.         }
  36.         // 初始化商品库存失败
  37.         return ResultBuildUtil.fail("601", "初始化商品库存失败,请确认redis库存是否已初始化!");
  38.     }
  39.     @GetMapping("/seckillBuy")
  40.     @ApiOperation(value = "2 开始秒杀", notes = "基于redis,RabbitMQ消息队列,实现有序秒杀抢购功能")
  41.     @ApiImplicitParams({
  42.         @ApiImplicitParam(value = "用户id",name = "userId"),
  43.         @ApiImplicitParam(value = "商品id",name = "prodId"),
  44.         @ApiImplicitParam(value = "抢购数量",name = "buyCount")
  45.     })
  46.     public RequestResult<?> seckillBuy(@RequestParam String userId,
  47.                                        @RequestParam String prodId,
  48.                                        @RequestParam Integer buyCount){
  49.         log.info("------ 用户:{},购买商品:{},购买数量:{},开始抢购 ------", userId, prodId, buyCount);
  50.         // TODO 增加请求许可校验(自定义注解+拦截器),校验请求此接口的所有用户是否是已登录用户,如果没有登录,提示请登录
  51.         // TODO 增加接口参数的校验,判断用户是否是系统正常用户,不能是状态异常用户,判断商品的数据是否正确(切记:涉及系统内数据不要信任请求参数,要信任系统的缓存的数据库)
  52.         // TODO 为了提高抢购入口的并发处理能力,要减少数据库交互,可以设计为根据商品编号,从redis缓存中查询商品,如果商品信息存在,则参与抢购,如果不存在,还是需要到数据库查询商品,如果数据库中存在,将商品信息存入redis缓存,如果数据库不存在,则直接提示抢购失败。
  53.         // TODO 此种场景,正常情况,没有问题,可能存在的问题,某个商品,是首次参与抢购,缓存中没有数据,但是数据库有,虽然上面的处理方式,可以解决,但是在高并发场景下,同一时刻会有大批量的请求来秒杀此商品,此时同时去缓存中获取商品数据,没有获取到,又同时去数据库查询,就会导致数据库扛不住压力,可能直接数据库挂掉。
  54.         // TODO 解决方式:缓存商品数据一般都是在后台添加抢购商品时,直接对商品进行预热处理,即:事先把参与抢购的商品直接同步到redis缓存中,这样当抢购开始,直接从redis缓存就可以获取到商品,而不会出现缓存击穿问题。
  55.         // TODO 虽然商品数据预热方式,可以解决此类问题,但是可能还会存在例外(比如:缓存中的商品由于后台失误操作,导致设置的过期时间不对,缓存时间提前过期,或者缓存数据误删除),此种情况还是需要当缓存不存在商品数据,从数据库查询,放入缓存的逻辑。
  56.         // TODO 解决方式:可以进行加锁,限制在高并发的情况下访问数据库,如果同一时刻,缓存中没有获取到商品数据库,就进入查询数据库操作,但是在进入查询前,增加分布式锁,只有获取到锁的请求,才可以查询数据库并加入到缓存中(加完就释放锁),获取不到锁的请求,直接返回失败(损失当前请求,后续请求进行弥补,只要一个操作成功,缓存中的数据就存在,后续的请求自然可以获取到数据)
  57.         // TODO 极端情况:redis无法使用,必须要增加redis的高可用,确保redis永远是有效的,考虑的模式就是集群模式下的哨兵机制。或者把大批量的请求直接放到消息队列,进行缓冲处理。
  58.         log.info("------------------------------ 进行请求用户数的统计计数,防止请求过多  -----------------------------------------");
  59.         // 增加业务逻辑处理:防止瞬时请求过多,使用redis的incr原子操作,进行请求用户数的统计计数,如果请求已经达到了上限(比如:设定请求用户数最多是抢购商品库存的2倍),其它后续所有的请求都默认无效,返回失败:当前请求用户过多,请稍后重试
  60.         if(seckillService.checkBuyUserCount(prodId)){
  61.             return ResultBuildUtil.fail("602", "抢购失败,当前抢购用户过多,请稍后重试!");
  62.         }
  63.         log.info("------------------------------            查看库存是否充足           -----------------------------------------");
  64.         //校验商品库存数量是否充足,可以进行后续抢购,日过不足,直接抢购失败
  65.         log.info("------ 用户:{},购买商品:{},购买数量:{},库存校验 ------", userId, prodId, buyCount);
  66.         if(!seckillService.checkProdStockEnough(prodId,buyCount)){
  67.             //库存不足,返回抢购失败
  68.             log.info("------ 用户:{},购买商品:{},购买数量:{},库存不足 ------", userId, prodId, buyCount);
  69.             return ResultBuildUtil.fail("603", "抢购失败,库存不足,缺货!");
  70.         }
  71.         log.info("------------------------------            判断用户是否抢购过           -----------------------------------------");
  72.         //增加幂等操作:当前抢购用户只能抢购一次,如果已经抢购过商品,不允许再次抢购(限制一个用户同一个抢购商品,整个抢购期间只能抢购一次)
  73.         log.info("------ 用户:{},购买商品:{},购买数量:{},锁定抢购用户,如果 已经抢购过商品,不允许再次抢购 ------", userId, prodId, buyCount);
  74.         if(seckillService.checkBuyUserBought(userId,prodId)){
  75.             //用户抢购过,返回抢购失败
  76.             log.info("------ 用户:{},购买商品:{},购买数量:{},重复抢购 ------", userId, prodId, buyCount);
  77.             return ResultBuildUtil.fail("604", "抢购失败,重复抢购!");
  78.         }
  79.         log.info("------------------------------            用户获取库存锁           -----------------------------------------");
  80.         //执行抢购业务,先给商品库存上锁(锁库存),如果上锁成功,代表当前用户继续抢购商品,如果上锁失败,说明有人抢购,进行等待
  81.         log.info("------ 用户:{},购买商品:{},购买数量:{},(尝试获取库存锁) 执行抢购业务,先给商品库存上锁(锁库存),拿到 库存锁 才可以开始下单 ------", userId, prodId, buyCount);
  82.         while (true){
  83.             //死循环,尝试锁库存,如果锁库存成功,代表库存所已经被释放
  84.             if(!seckillService.checkProdStockLocked(prodId)){
  85.                 log.info("------ 用户:{},购买商品:{},购买数量:{},锁库存成功,拿到 库存锁 ,开始下单------", userId, prodId, buyCount);
  86.                 //结束循环,执行抢购下单
  87.                 break;
  88.             }
  89.             log.debug("------ 用户:{},购买商品:{},购买数量:{},锁库存成功失败,等待。。。------", userId, prodId, buyCount);
  90.         }
  91.         log.info("------------------------------     已经获得库存锁,再次判断库存是否充足  -----------------------------------------");
  92.         //考虑高并发的场景:
  93.         //多人同时校验库存成功,但是进入到抢购下单业务时,库存呢只够部分人购买,需要再确定库存是否足够
  94.         //校验商品库存数量是否充足,可以进行后续抢购,日过不足,直接抢购失败
  95.         log.info("------ 用户:{},购买商品:{},购买数量:{},锁库存后(拿到库存锁后)  再次 库存校验 ------",userId, prodId, buyCount);
  96.         if(!seckillService.checkProdStockEnough(prodId,buyCount)){
  97.             //释放库存锁,后续用户继续尝试购买
  98.             //库存剩余2两,还有三个人买,库存不足,后续两个人各买一件
  99.             //释放库存锁
  100.             seckillService.unLockProdStock(prodId);
  101.             log.info("------ 用户:{},购买商品:{},购买数量:{},再次 库存校验,库存不足 ------", userId, prodId, buyCount);
  102.             //库存不足,返回抢购失败
  103.             log.info("------ 用户:{},购买商品:{},购买数量:{},抢购下单中,库存不足 ------", userId, prodId, buyCount);
  104.             return ResultBuildUtil.fail("605", "抢购失败,库存不足,缺货!");
  105.         }
  106.         log.info("------------------------------            开始扣减库存           -----------------------------------------");
  107.         //执行扣减商品库存
  108.         log.info("------ 用户:{},购买商品:{},购买数量:{},扣减库存 ------", userId, prodId, buyCount);
  109.         seckillService.subProdStockCount(prodId,buyCount);
  110.         log.info("------------------------------               开始下单           -----------------------------------------");
  111.         //开始调用订单中心的生成抢购订单接口,下单并返回给前端,抢购结果
  112.         //先模拟下单成功,返回一个抢购订单号
  113.         log.info("------ 用户:{},购买商品:{},购买数量:{},开始下单 ------", userId, prodId, buyCount);
  114. //        String seckillOrderNo = "SK"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
  115. //                + UUID.randomUUID().toString().substring(0,5);
  116.         String seckillOrder = seckillOrderFeignService.feignInvokeCreateSeckillOrder(userId, prodId, buyCount);
  117.         log.info("------------------------------       下单成功 主动释放库存锁        -----------------------------------------");
  118.         //生成抢购订单成功,立刻释放库存锁,给其他抢购用户购买
  119.         log.info("------ 用户:{},购买商品:{},购买数量:{},下单成功,释放库存锁 ------", userId, prodId, buyCount);
  120.         seckillService.unLockProdStock(prodId);
  121.         log.info("------ 用户:{},购买商品:{},购买数量:{},抢购成功 ------", userId, prodId, buyCount);
  122.         //返回抢购成功,实际开发不能返回此种格式的数据
  123.         //必须使用key和value的返回,方便前端获取订单号
  124.         return ResultBuildUtil.success("抢购成功,抢购订单"+seckillOrder);
  125.     }
  126. }
复制代码
3.3 订单模块


3.3.1 application.yml

点击查看代码
  1. # 端口
  2. server:
  3.   port: 8107
  4. # 服务名
  5. spring:
  6.   application:
  7.     name: edocmall-seckill-order
  8.   # redis
  9.   redis:
  10.     host: 127.0.0.1
  11.     port: 6379
  12.   # RabbitMQ
  13.   rabbitmq:
  14.     host: 1.117.75.57
  15.     port: 5672
  16.     username: admin
  17.     password: admin
  18. # eureka 注册中心的配置
  19. eureka:
  20.   client:
  21.     service-url:
  22.       defaultZone: http://127.0.0.1:8096/eureka  # 注册中心的地址
复制代码
3.3.2 OrderMQDirectConfig

点击查看代码
  1. /**
  2. * Created On : 1/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: Direct直连模式,自动配置类,自动创建队列,交换机,并将队列绑定到交换机,指定唯一路由
  7. */
  8. @Configuration
  9. public class OrderMQDirectConfig {
  10.     /**
  11.      * @author : huayu
  12.      * @date   : 1/11/2022
  13.      * @param  : []
  14.      * @return : org.springframework.amqp.core.Queue
  15.      * @description : directQueue
  16.      */
  17.     @Bean
  18.     public Queue directQueue(){
  19.         return new Queue(OrderMQConstant.SECKILL_SAVE_ORDER_QUEUE_KH96,true);
  20.     }
  21.     /**
  22.      * @author : huayu
  23.      * @date   : 1/11/2022
  24.      * @param  : []
  25.      * @return : org.springframework.amqp.core.DirectExchange
  26.      * @description : 创建直连交换机
  27.      */
  28.     @Bean
  29.     public DirectExchange directExchange(){
  30.         // 创建支持持久化的直连交换机,指定交换机的名称
  31.         return new DirectExchange(OrderMQConstant.SECKILL_SAVE_ORDER_EXCHANGE_KH96U);
  32.     }
  33.     /**
  34.      * @author : huayu
  35.      * @date   : 1/11/2022
  36.      * @param  : []
  37.      * @return : org.springframework.amqp.core.Binding
  38.      * @description : 将直连队列和直连交换机进行绑定,并指定绑定的唯一路由键
  39.      */
  40.     @Bean
  41.     public Binding directBinding(){
  42.         // 将直连队列和直连交换机进行绑定,并指定绑定的唯一路由键
  43.         return BindingBuilder.bind(directQueue())
  44.                             .to(directExchange())
  45.                             .with(OrderMQConstant.SECKILL_SAVE_ORDER_ROUTING_KH96);
  46.     }
  47. }
复制代码
3.3.3 OrderMQConstant

点击查看代码
  1. /**
  2. * Created On : 1/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: RabbitMQ 常量类,系统的所有队列名,交换机名,路由键名等,统一进行配置管理
  7. */
  8. public class OrderMQConstant {
  9.     //========================== 直连模式
  10.     /**
  11.      * Direct直连模式 队列名
  12.      */
  13.     public static final String SECKILL_SAVE_ORDER_QUEUE_KH96 ="seckill_save_order_queue_kh96";
  14.     /**
  15.      * Direct直连模式 交换机名
  16.      */
  17.     public static final String SECKILL_SAVE_ORDER_EXCHANGE_KH96U ="seckill_save_order_exchange_kh96";
  18.     /**
  19.      * Direct直连模式 路由键
  20.      */
  21.     public static final String SECKILL_SAVE_ORDER_ROUTING_KH96 ="seckill_save_order_routing_kh96";
  22.     //========================== 扇形模式
  23.     /**
  24.      * Fanout 扇形模式 队列名
  25.      */
  26.     public static final String ACCOUNT_FANOUT_QUEUE_KH96 ="account_pay_result_queue_kh96";
  27. }
复制代码
3.3.4 下订单业务层

3.3.4.1 接口

点击查看代码
  1. /**
  2. * Created On : 10/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description:
  7. */
  8. public interface SeckillOrderService {
  9.     /**
  10.      * @author : huayu
  11.      * @date   : 11/11/2022
  12.      * @param  : [seckillOrder]
  13.      * @return : void
  14.      * @description : 生成秒杀订单
  15.      */
  16.     void saveSeckillOrder(Map<String,Object> seckillOrder);
  17. }
复制代码
3.3.4.2实现类

点击查看代码
  1. /**
  2. * Created On : 10/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: SeckillOrderServiceImpl
  7. */
  8. @Service
  9. @Slf4j
  10. public class SeckillOrderServiceImpl implements SeckillOrderService {
  11.     @Autowired
  12.     private RabbitTemplate rabbitTemplate;
  13.     @Autowired
  14.     private RedisUtils redisUtils;
  15.     @Override
  16.     public void saveSeckillOrder(Map<String, Object> seckillOrder) {
  17.         //发送生成抢购订单的消息到消息队列,并在redis中添加此订单的记录,模拟交互
  18.         //0 正在生成
  19.         if(redisUtils.set(seckillOrder.get("seckillOrderNo").toString(),0)){
  20.             //发送生成订单的消息
  21.             rabbitTemplate.convertAndSend(OrderMQConstant.SECKILL_SAVE_ORDER_EXCHANGE_KH96U,
  22.                     OrderMQConstant.SECKILL_SAVE_ORDER_ROUTING_KH96,
  23.                     seckillOrder);
  24.         }
  25.     }
  26. }
复制代码
3.3.5 SeckillOrderSaveListener

点击查看代码
  1. /**
  2. * Created On : 1/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: Direct 直连模式消费者 One
  7. */
  8. @Slf4j
  9. @Component
  10. //指定接听的 消息队列 名字
  11. @RabbitListener(queues = OrderMQConstant.SECKILL_SAVE_ORDER_QUEUE_KH96)
  12. public class SeckillOrderSaveListener {
  13.     @Autowired
  14.     private RedisUtils redisUtils;
  15.     /**
  16.      * @author : huayu
  17.      * @date   : 1/11/2022
  18.      * @param  : [directMsgJson]
  19.      * @return : void
  20.      * @description : Direct 直连模式消费者One,消费信息
  21.      */
  22.     //指定消息队列中的消息,交给对应的方法处理
  23.     @RabbitHandler
  24.     public void saveSeckillOrder(Map<String,Object> seckillOrder){
  25.         log.info("***** 秒杀抢购订单:{},开始入库 ******",seckillOrder.get("seckillOrderNo"));
  26.         //TODO 将消息中的订单实体对象,调入业务接口,插入到数据库
  27.         redisUtils.set(seckillOrder.get("seckillOrderNo").toString(),1);
  28.         log.info("***** 秒杀抢购订单:{},入库成功 ******",seckillOrder.get("seckillOrderNo"));
  29.     }
  30. }
复制代码
3.3.6 下单 控制层

点击查看代码
  1. /**
  2. * Created On : 10/11/2022.
  3. * <p>
  4. * Author : huayu
  5. * <p>
  6. * Description: 秒杀抢购订单入口
  7. */
  8. @Slf4j
  9. @RestController
  10. public class SeckillOrderController {
  11.     @Autowired
  12.     private SeckillOrderService seckillOrderService;
  13.     /**
  14.      * @author : huayu
  15.      * @date   : 10/11/2022
  16.      * @param  : [userId, prodId, buyCount]
  17.      * @return : java.lang.String
  18.      * @description : 生成秒杀抢购订单
  19.      */
  20.     @GetMapping("/createSeckillOrder")
  21.     public String createSeckillOrder(@RequestParam String userId,
  22.                                      @RequestParam String prodId,
  23.                                      @RequestParam Integer buyCount){
  24.         log.info("****** 用户:{},购买商品:{},购买数量:{},生成抢购订单 ******", userId, prodId, buyCount);
  25.         //TODO 必须要有参数校验,必须要有用户,商品信息的校验,确定用户是否正常,商品是否还在抢购中
  26.         //TODO 再次强调所有的中心模块,数据来源,不能信任外部接口来源的参数,都必须从数据库或者缓存中获取,尤其是跟金钱相关
  27.         //TODO 所有的接口必需要校验结束,通过获取的数据,封装订单实体对象,用户不关系订单的生成业务,可以使用异步消息队列,实现晓峰,并快速响应
  28.         //模拟生成一个抢购订单号,并封装成订单实体对象,通过map集合模拟
  29.         String seckillOrderNo = "SK"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
  30.                 + UUID.randomUUID().toString().substring(0,5);
  31.         //创建订单实体对象
  32.         Map<String, Object> seckillOrder = new HashMap<>();
  33.         seckillOrder.put("userId",userId);
  34.         seckillOrder.put("prodId",prodId);
  35.         seckillOrder.put("buyCount",buyCount);
  36.         //TODO 其他的订单属性封装,比如收货人,总价格,优惠,时间等
  37.         seckillOrder.put("seckillOrderNo",seckillOrderNo);
  38.         //发送到生成订单的消息队列中,使用异步处理
  39.         seckillOrderService.saveSeckillOrder(seckillOrder);
  40.         //返回抢购订单
  41.         return  seckillOrder.toString();
  42.     }
  43. }
复制代码
3.4 查询订单状态和订单支付部分不再赘述

4、测试

4.1 初始化库存


4.2 抢购

4.2.1 抢购情况


4.2.2 redis中数据变化情况

4.3 查看订单详情



4.4 订单支付

支付的时候数注意穿透的路径;






免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4