草庐IT

SpringCloud(十一)- 秒杀 抢购

xiaoqigui 2023-04-16 原文

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 加锁

版本一#但是这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败。
setnx key value

版本二#给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。
setnx key value 
expire key seconds

版本三(推荐)#通过“set...nx...”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。
set key value nx ex seconds

2.1.2 解锁

解锁就是删除代表锁的那份数据。

del key

参考博客:https://blog.csdn.net/weixin_45525272/article/details/126562119


3、实现代码

主要是抢购的业务;

3.1模块分析

3.2 web模块

3.2.1 application.yml

点击查看代码
# 端口
server:
  port: 8106

# 服务名
spring:
  application:
    name: edocmall-seckill-web

# redis 配置
  redis:
    host: 127.0.0.1
    port: 6379

# eureka 注册中心的配置
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8096/eureka  # 注册中心的地址

# 秒杀抢购自定义配置
kh96:
  seckill:
    buy-user-count-prefix: seckill-buy-user-count # 请求用户数量的限制头标识
    buy-prod-stock-count: 100 # 初始化腔骨商品的库存数量,存入redis,实际开发中,没有此配置(初始化商品库存,在洗头膏添加抢购商品是)
    buy-prod-stock-prefix: seckill-buy-prod-stock # 抢购商品数量 头标识
    buy-user-lock-prefix: seckill-buy-user-lock # 锁定抢购用户的头标识
    buy-prod-lock-prefix: seckill-buy-prod-lock # 锁定商品库存锁头标识

3.2.2 SeckillConfig 配置类

点击查看代码
/**
 * Created On : 9/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: SeckillConfig
 */
@Data
@Component
@ConfigurationProperties(prefix = "kh96.seckill")
public class SeckillConfig {

    /*
        请求用户数量的限制头标识
     */
    private String  buyUserCountPrefix;

    /*
        初始化抢购商品的库存数量
     */
    private Integer buyProdStockCount;

    /*
        抢购商品数量 头标识
     */
    private String buyProdStockPrefix;

    /*
        锁定抢购用户的头标识
     */
    private String buyUserLockPrefix;

    /*
        锁定商品库存锁头标识
     */
    private String buyProdLockPrefix;

}

3.2.3 抢购接口和实现类

3.2.3.1 接口
点击查看代码
/**
 * Created On : 9/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 秒杀抢购的业务接口
 */
public interface SeckillService {


    /**
     * @author : huayu
     * @date   : 9/11/2022
     * @param  : [prodId, stockCount]
     * @return : void
     * @description : 初始化商品库存到缓存
     */
    boolean  initProdStock2Redis(String prodId,Integer stockCount);

    /**
     * @author : huayu
     * @date   : 9/11/2022
     * @param  : [productId]
     * @return : boolean
     * @description : 校验抢购商品的请求数量是否达到上限
     */
    boolean checkBuyUserCount(String productId);


    /**
     * @author : huayu
     * @date   : 9/11/2022
     * @param  : [prodId, buyCount]
     * @return : boolean
     * @description : 校验商品库存是否充足
     */
    boolean checkProdStockEnough(String prodId,Integer buyCount);

    /**
     * @author : huayu
     * @date   : 9/11/2022
     * @param  : [userId, prodId]
     * @return : boolean
     * @description : 校验用户是否已经抢购过当前商品
     */
    boolean checkBuyUserBought(String userId,String prodId);


    /**
     * @author : huayu
     * @date   : 9/11/2022
     * @param  : [userId]
     * @return : boolean
     * @description : 校验商品库存是否锁定
     */
    boolean checkProdStockLocked(String prodId);

    /**
     * @author : huayu
     * @date   : 9/11/2022
     * @param  : [prodId]
     * @return : boolean
     * @description : 释放库存锁
     */
    void unLockProdStock(String prodId);

    /**
     * @author : huayu
     * @date   : 9/11/2022
     * @param  : [prodId, butCount]
     * @return : void
     * @description : 扣减库存
     */
    void subProdStockCount(String prodId, Integer butCount);


}
3.2.3.2 实现类
点击查看代码
/**
 * Created On : 9/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 抢购业务接口实现类
 */
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private SeckillConfig seckillConfig;


    @Override
    public boolean initProdStock2Redis(String prodId, Integer stockCount) {
        // 判断redis中,是否存在已经初始化的商品库存数据,如果已经存在,不需要再次初始化
        if (ObjectUtils.isEmpty(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId))){

            // 将商品库存添加到redis中
            return redisUtils.set(seckillConfig.getBuyProdStockPrefix() + ":" + prodId, stockCount);
        }

        // 已经存在商品库存,不需要设置
        return false;
    }


    @Override
    public boolean checkBuyUserCount(String prodId) {
        // 使用redis的incr操作,校验当前商品的抢购用户数是否达到上限
        return redisUtils.incr(seckillConfig.getBuyUserCountPrefix() + ":" + prodId, 1)
                > Integer.parseInt(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId).toString()) * 2;
    }


    @Override
    public boolean checkProdStockEnough(String prodId, Integer buyCount) {

        //校验商品库存,是否大于等于用户抢购数量,如果小于,代表库存不足
        //TODO 如果redis 没有查到库存(库存没初始化,或者后台误删,必须要进行数据库查询操作,获取库存,
        // 要加锁,只有一个线程取操作),再同步到redis

        return Integer.parseInt(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId).toString())
                >= buyCount;

    }

    @Override
    public boolean checkBuyUserBought(String userId, String prodId) {

        //使用redis的分布式锁,sexnx,
        // 如果上锁成功,代表没有买过,可以继续购买,如果上锁失败,表示购买过不能继续购买
        //锁时长为正抢购活动时长
        return ! redisUtils.lock( seckillConfig.getBuyUserLockPrefix()+":"+userId,
                                null,
                                15*60); //模拟 抢购时长
    }

    @Override
    public boolean checkProdStockLocked(String prodId) {
        //使用redis的分布式锁,sexnx,如果上锁成功,代表库存没有被锁,如果上锁失败代表库存被其他用户锁定不能购买
        //锁库存必须要增加时长限制,防止库存锁释放失败,导致用户无法抢购,过期仍然会释放
        return ! redisUtils.lock(seckillConfig.getBuyProdLockPrefix()+":"+prodId,
                                null,
                                2*60); //模拟锁2分钟

    }

    @Override
    public void unLockProdStock(String prodId) {
        //删除redis中的 库存锁
        redisUtils.del(seckillConfig.getBuyProdLockPrefix()+":"+prodId);
    }

    @Override
    public void subProdStockCount(String prodId, Integer butCount) {
        //扣减 redis中缓存的商品库存数量
        //TODO redis缓存中操成功后,要发异步消息 到队列  更新数据库中商品库存呢
        redisUtils.decr(seckillConfig.getBuyProdStockPrefix() + ":" + prodId,
                butCount);

    }

}

3.2.4 远程调用订单模块进行下单

点击查看代码
/**
 * Created On : 10/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: SeckillOrderFeignService
 */
@FeignClient(value = "edocmall-seckill-order")
public interface SeckillOrderFeignService {


    /**
     * @author : huayu
     * @date   : 10/11/2022
     * @param  : [userId, prodId, buyCount]
     * @return : java.lang.String
     * @description : 远程调用订单中心,生成秒杀抢购订单的接口
     */
    @GetMapping("/createSeckillOrder")
    String feignInvokeCreateSeckillOrder(@RequestParam(value = "userId") String userId,
                              @RequestParam(value = "prodId") String prodId,
                              @RequestParam(value = "buyCount") Integer buyCount);

   

}

3.2.5 抢购 控制层

点击查看代码
/**
 * Created On : 9/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 秒杀操作入口
 */
@Slf4j
@RestController
@Api(tags = "秒杀操作")
public class SeckillController {


    @Autowired
    private SeckillService seckillService;

    @Autowired
    private SeckillOrderFeignService seckillOrderFeignService;

    /**
     * @author : zhukang
     * @date   : 2022/11/9
     * @param  : [userId, prodId, buyCount]
     * @return : com.kgc.scd.util.RequestResult<?>
     * @description : 模拟初始化商品库存,实际开发中,没有此操作,是在后台添加抢购商品预热是,直接同步到redis
     */
    @GetMapping("/initProdStock")
    @ApiOperation(value = "1 初始库存", notes = "抢购商品预热,初始化商品库存数量到redis中")
    @ApiImplicitParams({
            @ApiImplicitParam(value = "初始化商品id",name = "prodId"),
            @ApiImplicitParam(value = "初始化商品数量",name = "stockCount")
    })
    public RequestResult<?> initProdStock(@RequestParam String prodId,
                                          @RequestParam Integer stockCount){

        log.info("------ 初始化商品:{},库存数量:{},到redis缓存中 ------", prodId, stockCount);

        // 增加业务逻辑处理:防止瞬时请求过多,使用redis的incr原子操作,进行请求用户数的统计计数,如果请求已经达到了上限(比如:设定请求用户数最多是抢购商品库存的2倍),其它后续所有的请求都默认无效,返回失败:当前请求用户过多,请稍后重试
        if(seckillService.initProdStock2Redis(prodId, stockCount)){
            return ResultBuildUtil.success("初始化商品库存成功!");
        }
        // 初始化商品库存失败
        return ResultBuildUtil.fail("601", "初始化商品库存失败,请确认redis库存是否已初始化!");
    }


    @GetMapping("/seckillBuy")
    @ApiOperation(value = "2 开始秒杀", notes = "基于redis,RabbitMQ消息队列,实现有序秒杀抢购功能")
    @ApiImplicitParams({
        @ApiImplicitParam(value = "用户id",name = "userId"),
        @ApiImplicitParam(value = "商品id",name = "prodId"),
        @ApiImplicitParam(value = "抢购数量",name = "buyCount")
    })
    public RequestResult<?> seckillBuy(@RequestParam String userId,
                                       @RequestParam String prodId,
                                       @RequestParam Integer buyCount){

        log.info("------ 用户:{},购买商品:{},购买数量:{},开始抢购 ------", userId, prodId, buyCount);

        // TODO 增加请求许可校验(自定义注解+拦截器),校验请求此接口的所有用户是否是已登录用户,如果没有登录,提示请登录
        // TODO 增加接口参数的校验,判断用户是否是系统正常用户,不能是状态异常用户,判断商品的数据是否正确(切记:涉及系统内数据不要信任请求参数,要信任系统的缓存的数据库)

        // TODO 为了提高抢购入口的并发处理能力,要减少数据库交互,可以设计为根据商品编号,从redis缓存中查询商品,如果商品信息存在,则参与抢购,如果不存在,还是需要到数据库查询商品,如果数据库中存在,将商品信息存入redis缓存,如果数据库不存在,则直接提示抢购失败。
        // TODO 此种场景,正常情况,没有问题,可能存在的问题,某个商品,是首次参与抢购,缓存中没有数据,但是数据库有,虽然上面的处理方式,可以解决,但是在高并发场景下,同一时刻会有大批量的请求来秒杀此商品,此时同时去缓存中获取商品数据,没有获取到,又同时去数据库查询,就会导致数据库扛不住压力,可能直接数据库挂掉。
        // TODO 解决方式:缓存商品数据一般都是在后台添加抢购商品时,直接对商品进行预热处理,即:事先把参与抢购的商品直接同步到redis缓存中,这样当抢购开始,直接从redis缓存就可以获取到商品,而不会出现缓存击穿问题。
        // TODO 虽然商品数据预热方式,可以解决此类问题,但是可能还会存在例外(比如:缓存中的商品由于后台失误操作,导致设置的过期时间不对,缓存时间提前过期,或者缓存数据误删除),此种情况还是需要当缓存不存在商品数据,从数据库查询,放入缓存的逻辑。
        // TODO 解决方式:可以进行加锁,限制在高并发的情况下访问数据库,如果同一时刻,缓存中没有获取到商品数据库,就进入查询数据库操作,但是在进入查询前,增加分布式锁,只有获取到锁的请求,才可以查询数据库并加入到缓存中(加完就释放锁),获取不到锁的请求,直接返回失败(损失当前请求,后续请求进行弥补,只要一个操作成功,缓存中的数据就存在,后续的请求自然可以获取到数据)
        // TODO 极端情况:redis无法使用,必须要增加redis的高可用,确保redis永远是有效的,考虑的模式就是集群模式下的哨兵机制。或者把大批量的请求直接放到消息队列,进行缓冲处理。

        log.info("------------------------------ 进行请求用户数的统计计数,防止请求过多  -----------------------------------------");
        // 增加业务逻辑处理:防止瞬时请求过多,使用redis的incr原子操作,进行请求用户数的统计计数,如果请求已经达到了上限(比如:设定请求用户数最多是抢购商品库存的2倍),其它后续所有的请求都默认无效,返回失败:当前请求用户过多,请稍后重试
        if(seckillService.checkBuyUserCount(prodId)){
            return ResultBuildUtil.fail("602", "抢购失败,当前抢购用户过多,请稍后重试!");
        }

        log.info("------------------------------            查看库存是否充足           -----------------------------------------");
        //校验商品库存数量是否充足,可以进行后续抢购,日过不足,直接抢购失败
        log.info("------ 用户:{},购买商品:{},购买数量:{},库存校验 ------", userId, prodId, buyCount);
        if(!seckillService.checkProdStockEnough(prodId,buyCount)){
            //库存不足,返回抢购失败
            log.info("------ 用户:{},购买商品:{},购买数量:{},库存不足 ------", userId, prodId, buyCount);
            return ResultBuildUtil.fail("603", "抢购失败,库存不足,缺货!");
        }

        log.info("------------------------------            判断用户是否抢购过           -----------------------------------------");
        //增加幂等操作:当前抢购用户只能抢购一次,如果已经抢购过商品,不允许再次抢购(限制一个用户同一个抢购商品,整个抢购期间只能抢购一次)
        log.info("------ 用户:{},购买商品:{},购买数量:{},锁定抢购用户,如果 已经抢购过商品,不允许再次抢购 ------", userId, prodId, buyCount);
        if(seckillService.checkBuyUserBought(userId,prodId)){
            //用户抢购过,返回抢购失败
            log.info("------ 用户:{},购买商品:{},购买数量:{},重复抢购 ------", userId, prodId, buyCount);
            return ResultBuildUtil.fail("604", "抢购失败,重复抢购!");
        }

        log.info("------------------------------            用户获取库存锁           -----------------------------------------");
        //执行抢购业务,先给商品库存上锁(锁库存),如果上锁成功,代表当前用户继续抢购商品,如果上锁失败,说明有人抢购,进行等待
        log.info("------ 用户:{},购买商品:{},购买数量:{},(尝试获取库存锁) 执行抢购业务,先给商品库存上锁(锁库存),拿到 库存锁 才可以开始下单 ------", userId, prodId, buyCount);
        while (true){
            //死循环,尝试锁库存,如果锁库存成功,代表库存所已经被释放
            if(!seckillService.checkProdStockLocked(prodId)){
                log.info("------ 用户:{},购买商品:{},购买数量:{},锁库存成功,拿到 库存锁 ,开始下单------", userId, prodId, buyCount);
                //结束循环,执行抢购下单
                break;
            }
            log.debug("------ 用户:{},购买商品:{},购买数量:{},锁库存成功失败,等待。。。------", userId, prodId, buyCount);
        }

        log.info("------------------------------     已经获得库存锁,再次判断库存是否充足  -----------------------------------------");
        //考虑高并发的场景:
        //多人同时校验库存成功,但是进入到抢购下单业务时,库存呢只够部分人购买,需要再确定库存是否足够
        //校验商品库存数量是否充足,可以进行后续抢购,日过不足,直接抢购失败
        log.info("------ 用户:{},购买商品:{},购买数量:{},锁库存后(拿到库存锁后)  再次 库存校验 ------",userId, prodId, buyCount);
        if(!seckillService.checkProdStockEnough(prodId,buyCount)){
            //释放库存锁,后续用户继续尝试购买
            //库存剩余2两,还有三个人买,库存不足,后续两个人各买一件
            //释放库存锁
            seckillService.unLockProdStock(prodId);
            log.info("------ 用户:{},购买商品:{},购买数量:{},再次 库存校验,库存不足 ------", userId, prodId, buyCount);

            //库存不足,返回抢购失败
            log.info("------ 用户:{},购买商品:{},购买数量:{},抢购下单中,库存不足 ------", userId, prodId, buyCount);
            return ResultBuildUtil.fail("605", "抢购失败,库存不足,缺货!");
        }

        log.info("------------------------------            开始扣减库存           -----------------------------------------");
        //执行扣减商品库存
        log.info("------ 用户:{},购买商品:{},购买数量:{},扣减库存 ------", userId, prodId, buyCount);
        seckillService.subProdStockCount(prodId,buyCount);


        log.info("------------------------------               开始下单           -----------------------------------------");
        //开始调用订单中心的生成抢购订单接口,下单并返回给前端,抢购结果
        //先模拟下单成功,返回一个抢购订单号
        log.info("------ 用户:{},购买商品:{},购买数量:{},开始下单 ------", userId, prodId, buyCount);
//        String seckillOrderNo = "SK"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
//                + UUID.randomUUID().toString().substring(0,5);

        String seckillOrder = seckillOrderFeignService.feignInvokeCreateSeckillOrder(userId, prodId, buyCount);

        log.info("------------------------------       下单成功 主动释放库存锁        -----------------------------------------");
        //生成抢购订单成功,立刻释放库存锁,给其他抢购用户购买
        log.info("------ 用户:{},购买商品:{},购买数量:{},下单成功,释放库存锁 ------", userId, prodId, buyCount);
        seckillService.unLockProdStock(prodId);

        log.info("------ 用户:{},购买商品:{},购买数量:{},抢购成功 ------", userId, prodId, buyCount);

        //返回抢购成功,实际开发不能返回此种格式的数据
        //必须使用key和value的返回,方便前端获取订单号
        return ResultBuildUtil.success("抢购成功,抢购订单"+seckillOrder);
    }



}

3.3 订单模块

3.3.1 application.yml

点击查看代码
# 端口
server:
  port: 8107

# 服务名
spring:
  application:
    name: edocmall-seckill-order

  # redis
  redis:
    host: 127.0.0.1
    port: 6379

  # RabbitMQ
  rabbitmq:
    host: 1.117.75.57
    port: 5672
    username: admin
    password: admin

# eureka 注册中心的配置
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8096/eureka  # 注册中心的地址

3.3.2 OrderMQDirectConfig

点击查看代码
/**
 * Created On : 1/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: Direct直连模式,自动配置类,自动创建队列,交换机,并将队列绑定到交换机,指定唯一路由
 */
@Configuration
public class OrderMQDirectConfig {

    /**
     * @author : huayu
     * @date   : 1/11/2022
     * @param  : []
     * @return : org.springframework.amqp.core.Queue
     * @description : directQueue
     */
    @Bean
    public Queue directQueue(){

        return new Queue(OrderMQConstant.SECKILL_SAVE_ORDER_QUEUE_KH96,true);
    }

    /**
     * @author : huayu
     * @date   : 1/11/2022
     * @param  : []
     * @return : org.springframework.amqp.core.DirectExchange
     * @description : 创建直连交换机
     */
    @Bean
    public DirectExchange directExchange(){
        // 创建支持持久化的直连交换机,指定交换机的名称
        return new DirectExchange(OrderMQConstant.SECKILL_SAVE_ORDER_EXCHANGE_KH96U);
    }

    /**
     * @author : huayu
     * @date   : 1/11/2022
     * @param  : []
     * @return : org.springframework.amqp.core.Binding
     * @description : 将直连队列和直连交换机进行绑定,并指定绑定的唯一路由键
     */
    @Bean
    public Binding directBinding(){
        // 将直连队列和直连交换机进行绑定,并指定绑定的唯一路由键
        return BindingBuilder.bind(directQueue())
                            .to(directExchange())
                            .with(OrderMQConstant.SECKILL_SAVE_ORDER_ROUTING_KH96);
    }


}

3.3.3 OrderMQConstant

点击查看代码
/**
 * Created On : 1/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: RabbitMQ 常量类,系统的所有队列名,交换机名,路由键名等,统一进行配置管理
 */
public class OrderMQConstant {

    //========================== 直连模式
    /**
     * Direct直连模式 队列名
     */
    public static final String SECKILL_SAVE_ORDER_QUEUE_KH96 ="seckill_save_order_queue_kh96";

    /**
     * Direct直连模式 交换机名
     */
    public static final String SECKILL_SAVE_ORDER_EXCHANGE_KH96U ="seckill_save_order_exchange_kh96";

    /**
     * Direct直连模式 路由键
     */
    public static final String SECKILL_SAVE_ORDER_ROUTING_KH96 ="seckill_save_order_routing_kh96";


    //========================== 扇形模式

    /**
     * Fanout 扇形模式 队列名
     */
    public static final String ACCOUNT_FANOUT_QUEUE_KH96 ="account_pay_result_queue_kh96";

}

3.3.4 下订单业务层

3.3.4.1 接口
点击查看代码
/**
 * Created On : 10/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description:
 */
public interface SeckillOrderService {

    /**
     * @author : huayu
     * @date   : 11/11/2022
     * @param  : [seckillOrder]
     * @return : void
     * @description : 生成秒杀订单
     */
    void saveSeckillOrder(Map<String,Object> seckillOrder);

}
3.3.4.2实现类
点击查看代码
/**
 * Created On : 10/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: SeckillOrderServiceImpl
 */
@Service
@Slf4j
public class SeckillOrderServiceImpl implements SeckillOrderService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisUtils redisUtils;


    @Override
    public void saveSeckillOrder(Map<String, Object> seckillOrder) {

        //发送生成抢购订单的消息到消息队列,并在redis中添加此订单的记录,模拟交互
        //0 正在生成
        if(redisUtils.set(seckillOrder.get("seckillOrderNo").toString(),0)){
            //发送生成订单的消息
            rabbitTemplate.convertAndSend(OrderMQConstant.SECKILL_SAVE_ORDER_EXCHANGE_KH96U,
                    OrderMQConstant.SECKILL_SAVE_ORDER_ROUTING_KH96,
                    seckillOrder);

        }

    }


}

3.3.5 SeckillOrderSaveListener

点击查看代码
/**
 * Created On : 1/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: Direct 直连模式消费者 One
 */
@Slf4j
@Component
//指定接听的 消息队列 名字
@RabbitListener(queues = OrderMQConstant.SECKILL_SAVE_ORDER_QUEUE_KH96)
public class SeckillOrderSaveListener {

    @Autowired
    private RedisUtils redisUtils;

    /**
     * @author : huayu
     * @date   : 1/11/2022
     * @param  : [directMsgJson]
     * @return : void
     * @description : Direct 直连模式消费者One,消费信息
     */
    //指定消息队列中的消息,交给对应的方法处理
    @RabbitHandler
    public void saveSeckillOrder(Map<String,Object> seckillOrder){
        log.info("***** 秒杀抢购订单:{},开始入库 ******",seckillOrder.get("seckillOrderNo"));

        //TODO 将消息中的订单实体对象,调入业务接口,插入到数据库

        redisUtils.set(seckillOrder.get("seckillOrderNo").toString(),1);

        log.info("***** 秒杀抢购订单:{},入库成功 ******",seckillOrder.get("seckillOrderNo"));


    }


}

3.3.6 下单 控制层

点击查看代码
/**
 * Created On : 10/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 秒杀抢购订单入口
 */
@Slf4j
@RestController
public class SeckillOrderController {

    @Autowired
    private SeckillOrderService seckillOrderService;


    /**
     * @author : huayu
     * @date   : 10/11/2022
     * @param  : [userId, prodId, buyCount]
     * @return : java.lang.String
     * @description : 生成秒杀抢购订单
     */
    @GetMapping("/createSeckillOrder")
    public String createSeckillOrder(@RequestParam String userId,
                                     @RequestParam String prodId,
                                     @RequestParam Integer buyCount){

        log.info("****** 用户:{},购买商品:{},购买数量:{},生成抢购订单 ******", userId, prodId, buyCount);

        //TODO 必须要有参数校验,必须要有用户,商品信息的校验,确定用户是否正常,商品是否还在抢购中
        //TODO 再次强调所有的中心模块,数据来源,不能信任外部接口来源的参数,都必须从数据库或者缓存中获取,尤其是跟金钱相关
        //TODO 所有的接口必需要校验结束,通过获取的数据,封装订单实体对象,用户不关系订单的生成业务,可以使用异步消息队列,实现晓峰,并快速响应

        //模拟生成一个抢购订单号,并封装成订单实体对象,通过map集合模拟
        String seckillOrderNo = "SK"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
                + UUID.randomUUID().toString().substring(0,5);

        //创建订单实体对象
        Map<String, Object> seckillOrder = new HashMap<>();
        seckillOrder.put("userId",userId);
        seckillOrder.put("prodId",prodId);
        seckillOrder.put("buyCount",buyCount);

        //TODO 其他的订单属性封装,比如收货人,总价格,优惠,时间等
        seckillOrder.put("seckillOrderNo",seckillOrderNo);

        //发送到生成订单的消息队列中,使用异步处理
        seckillOrderService.saveSeckillOrder(seckillOrder);

        //返回抢购订单
        return  seckillOrder.toString();

    }

}

3.4 查询订单状态和订单支付部分不再赘述

4、测试

4.1 初始化库存

4.2 抢购

4.2.1 抢购情况

4.2.2 redis中数据变化情况

4.3 查看订单详情

4.4 订单支付

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




有关SpringCloud(十一)- 秒杀 抢购的更多相关文章

  1. 【云原生】SpringCloud-Spring Boot Starter使用测试 - 2

    目录SpringBootStarter是什么?以前传统的做法使用SpringBootStarter之后starter的理念:starter的实现: 创建SpringBootStarter步骤在idea新建一个starter项目、直接执行下一步即可生成项目。 在xml中加入如下配置文件:创建proterties类来保存配置信息创建业务类:创建AutoConfiguration测试如下:SpringBootStarter是什么? SpringBootStarter是在SpringBoot组件中被提出来的一种概念、简化了很多烦琐的配置、通过引入各种SpringBootStarter包可以快速搭建出一

  2. SpringCloud入门实战(七)-Hystrix入门简介 - 2

    📝学技术、更要掌握学习的方法,一起学习,让进步发生👩🏻作者:一只IT攻城狮。💐学习建议:1、养成习惯,学习java的任何一个技术,都可以先去官网先看看,更准确、更专业。💐学习建议:2、然后记住每个技术最关键的特性(通常一句话或者几个字),从主线入手,由浅入深学习。❤️《SpringCloud入门实战系列》解锁SpringCloud主流组件入门应用及关键特性。带你了解SpringCloud主流组件,是如何一战解决微服务诸多难题的。项目demo:源码地址👉🏻SpringCloud入门实战系列不迷路👈🏻:SpringCloud入门实战(一)什么是SpringCloud?SpringCloud入门实战

  3. 手把手教你搭建SpringCloud Alibaba之生产者与消费者 - 2

      SpringCloudAlibaba全集文章目录:零、手把手教你搭建SpringCloudAlibaba项目一、手把手教你搭建SpringCloudAlibaba之生产者与消费者二、手把手教你搭建SpringCloudAlibaba之Nacos服务注册中心三、手把手教你搭建SpringCloudAlibaba之Nacos服务配置中心四、手把手教你搭建SpringCloudAlibaba之Nacos服务集群配置五、手把手教你搭建SpringCloudAlibaba之Nacos服务持久化配置六、手把手教你搭建SpringCloudAlibaba之Sentinel实现流量实时监控七、手把手教你搭

  4. 工程(十一)——NUC11+D435i+VINS-FUSION+ESDF建图(github代码) - 2

    博主的合并代码git@github.com:huashu996/VINS-FUSION-ESDFmap.git一、D435i深度相机配置1.1SDK+ROS参考我之前的博客,步骤和所遇见的问题已经写的很详细了https://blog.csdn.net/HUASHUDEYANJING/article/details/129323834?spm=1001.2014.3001.55011.2相机标定参数1、相机内参通过rostopic的camera/info获取header:标准消息头seq:序列ID,连续递增的ID号stamp:两个时间戳frame_id:与此数据相关联的帧IDheight:图像尺

  5. ElasticSearch(十一)【集群搭建】 - 2

    十一、ES集群的相关概念上一篇文章《ElasticSearch-聚合查询》集群(cluster)一个集群就是由一个或多个节点组织在一起,它们共同持有你整个的数据,并一起提供索引和搜整合应用索功能。一个集群由一个唯一的名字标识,这个名字默认就是elasticsearch。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群节点(node)一个节点是集群中的一个服务器,作为集群的一部分,它存储你的数据,参与集群的索引节点和搜索功能。和集群类似,一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点索引(Index)一组

  6. 计算机网络(十一)——导论与路由器工作原理 - 2

    文章目录1.网络层的概述2.路由器的工作原理2.1路由器的组成2.2转发2.2.1查找2.2.2交换2.2.3输出端口处理核心内容路由器的工作原理1.网络层的概述(1)功能结构网络层用于提供主机与主机之间的逻辑通信,源主机网络层接收来自运输层的报文段,将其封装为一个数据报并向相邻的路由器发送数据报。接收方的网络层接收来自相邻路由器的数据报,提取出报文段交付给运输层。为此需要提供转发和路由选择两个功能。网络层能够被划分为两个相互作用的部分,数据部分和路由部分:数据部分的主要作用:转发,即分组从输入链路接口转发到合适的输出链路接口。控制部分的主要作用:路由选择,即决定从源主机到目的主机,分组所采用

  7. STM32_基础入门(十一)第二篇_通用定时器使用详解 - 2

    持续关注阿杰在线更新保姆式笔记~~坚持日更目录一、通用定时器基本介绍二、基本定时功能1、定时器时钟来源分析2、常用库函数3、代码区三、定时器输出PWM3.1基本介绍3.2 PWM工作过程​3.3 常用库函数 ​PWM输出配置步骤: 3.4 代码区四、输入捕获功能1.基本介绍2.工作过程3.常用库函数 输入捕获的一般配置步骤 代码区一、通用定时器基本介绍通用定时器包括TIM2、TIM3、TIM4和TIM5STM32通用定时器是一个通过可编程预分频器驱动的16位自动装载计数器构成。每个定时器都是完全独立的,没有互相共享任何资源。它们可以一起同步操作。定时器可以进行定时器基本定时,输出4路PWM,输

  8. Docker部署springcloud项目(清晰明了) - 2

    概述最近在想做个cloud项目,gitee上找了个模板项目,后端使用到Nacos、Gateway、Security等技术,需要到Docker容器部署,在此总结一下,若有不足之处,望大佬们可以指出。什么是DockerDocker使用Google公司推出的Go语言进行开发实现,基于Linux内核的cgroup,namespace,以及AUFS类的UnionFS等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。Docker在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得Do

  9. Javascript getDay 返回四月、六月、九月、十一月的错误值 - 2

    我正在使用位于此处的脚本:http://www.javascriptkit.com/script/script2/dyndateselector.shtml如果您尝试并转到4月、6月、9月或11月中的任何一个,您会发现星期几列不正确。这是不正确数据的列表(x开始y的东西显示下个月。)被窃听的月份:4月4日(星期日而不是星期五开始)五月从周日开始6月6日(周五而不是周三开始)七月从星期五开始9/9月(星期六而不是星期四开始)十月从星期六开始11/11月(星期四而不是星期二开始)十二月从星期四开始您会注意到每个有错误的月份都是从下个月的那一天开始的,但所有其他月份似乎都是正确的。我找不到关

  10. 【web系列十一】使用django创建数据库表 - 2

    目录基本介绍ModelORM创建数据库的流程安装插件        安装python中操作MySQL的库,这里用了django官方推荐的mysqlclient创建数据库连接数据库1、工程同名app下的settings.py2、子应用的models.py3、子应用中的admin.py生成数据表1、更新数据表变化情况2、生成/更新数据表3、版本回退详解Model语法字段定义外键参数其他一些参数的含义数据库操作添加数据获取数据更新数据更新包含外键的数据删除数据更新数据表结构方法1:先删除再重构方法2:新增字段可以直接在原结构上添加问题记录django中获取的当前时间被保存到mysql数据库中会有时差

随机推荐