秒杀高并发解决方案

news2024/11/23 22:59:52

秒杀高并发解决方案

1.秒杀/高并发方案-介绍

  1. 秒杀/高并发 其实主要解决两个问题,一个是并发读,一个是并发写
  2. 并发读的核心优化理念是尽量减少用户到 DB 来"读"数据,或者让他们读更少的数据, 并
    发写的处理原则也一样
  3. 针对秒杀系统需要做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况
    发生。
  4. 系统架构要满足高可用: 流量符合预期时要稳定,要保证秒杀活动顺利完成,即秒杀商品
    顺利地卖出去,这个是最基本的前提
  5. 系统保证数据的一致性: 就是秒杀 10 个 商品 ,那就只能成交 10 个商品,多一个少一
    个都不行。一旦库存不对,就要承担损失
  6. 系统要满足高性能: 也就是系统的性能要足够高,需要支撑大流量, 不光是服务端要做极
    致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点, 整个系统就"快
    "了
  7. 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键, 对应的方案比如页
    面缓存方案、Redis 预减库存/内存标记与隔离、请求的削峰(RabbitMQ/异步请求)、分布式
    Session 共享等

image-20230226140153636

image-20230226140537788

2.秒杀场景模拟

image-20230226180110014

image-20230226180155154

image-20230226180138413

这里模拟每秒1000个请求,循环10次即总共发起10000次请求,请求/seckill/doSeckill接口

3.秒杀解决方案

3.1 v1.0-原始版本

SeckillController

@Controller
@RequestMapping(value = "/seckill")
public class SeckillController {

    @Resource
    private GoodsService goodsService;

    @Resource
    private SeckillOrderService seckillOrderService;

    @Resource
    private OrderService orderService;

    @RequestMapping(value = "/doSeckill")
    public String doSeckill(Model model, User user, Long goodsId) {
        System.out.println("-----秒杀 V1.0--------");
        //===================秒杀 v1.0 start =========================
        if (user == null) {
            return "login";
        }
        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        //判断库存
        if (goodsVo.getStockCount() < 1) {
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }
        //解决重复抢购
        LambdaQueryWrapper<SeckillOrder> seckillOrderLambdaQueryWrapper = new QueryWrapper<SeckillOrder>().lambda()
                .eq(SeckillOrder::getGoodsId, goodsId).eq(SeckillOrder::getUserId, user.getId());
        SeckillOrder seckillOrder = seckillOrderService.getOne(seckillOrderLambdaQueryWrapper);
        if (seckillOrder != null) {
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }
        //抢购
        Order order = orderService.seckill(user, goodsVo);
        if (order == null) {
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }
        model.addAttribute("order", order);
        model.addAttribute("goods", goodsVo);
        return "orderDetail";
        //===================秒杀 v1.0 end... =========================
    }

}

OrderServiceImpl

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    @Resource
    private SecKillGoodsService secKillGoodsService;

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private SeckillOrderMapper seckillOrderMapper;

    /**
     * 秒杀商品,减少库存
     * @param user
     * @param goodsVo
     * @return
     */
    @Override
    public Order seckill(User user, GoodsVo goodsVo) {
        //1.根据商品id获取秒杀商品信息
        LambdaQueryWrapper<SecKillGoods> lambdaQueryWrapper = new QueryWrapper<SecKillGoods>().lambda().eq(SecKillGoods::getGoodsId,goodsVo.getId());
        SecKillGoods seckillGoods = secKillGoodsService.getOne(lambdaQueryWrapper);
        //2.秒杀商品库存-1
        seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
        secKillGoodsService.updateById(seckillGoods);
        //3.生成普通订单
        Order order = new Order();
        order.setUserId(user.getId());
        order.setGoodsId(goodsVo.getId());
        order.setDeliveryAddrId(0L);
        order.setGoodsName(goodsVo.getGoodsName());
        order.setGoodsCount(1);
        order.setGoodsPrice(seckillGoods.getSeckillPrice());
        order.setOrderChannel(1);
        order.setStatus(0);
        order.setCreateDate(new Date());
        orderMapper.insert(order);
        //4.生成秒杀订单
        SeckillOrder seckillOrder = new SeckillOrder();
        seckillOrder.setGoodsId(goodsVo.getId());
        seckillOrder.setUserId(user.getId());
        seckillOrder.setOrderId(order.getId());
        seckillOrderMapper.insert(seckillOrder);
        return order;
    }
}

说明

非高并发情况下,程序执行流程分析

  1. 程序首先校验用户是否登录

  2. 然后判断商品库存是充足

  3. 在判断是否存在重复抢购

  4. 最后执行抢购生成订单及抢购订单数据

高并发情况下,程序执行流程分析

  1. 判断商品库存是充足、否存在重复抢购的程序是不具备原子性
  2. 秒杀商品的库存为负数,但具体的值无法确定,因为获取秒杀商品库存也不具备原子性
  3. 当有多少个请求冲破了前面库存和抢购的过滤,就会去生成多少个订单

3.2 v2.0-秒杀-复购、超卖处理

SeckillController

@RequestMapping(value = "/doSeckill")
public String doSeckill(Model model, User user, Long goodsId) {
    System.out.println("-----秒杀 V2.0--------");
    //===================秒杀 v2.0 start =========================
    if (user == null) {
        return "login";
    }
    model.addAttribute("user", user);
    GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
    //判断库存
    if (goodsVo.getStockCount() < 1) {
        model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
        return "secKillFail";
    }
    //解决重复抢购,直接到redis中获取对应的秒杀订单,如果有则说明已经抢购了,不能继续抢购
    SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
    if (seckillOrder != null) {
        model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
        return "secKillFail";
    }

    //抢购
    Order order = orderService.seckill(user, goodsVo);
    if (order == null) {
        model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
        return "secKillFail";
    }
    model.addAttribute("order", order);
    model.addAttribute("goods", goodsVo);
    return "orderDetail";
    //===================秒杀 v2.0 end... =========================
}

OrderServiceImpl

/**
 * 秒杀商品,减少库存
 *
 * @param user
 * @param goodsVo
 * @return
 */
@Override
@Transactional(rollbackFor = {Exception.class})
public Order seckill(User user, GoodsVo goodsVo) {
    //1.根据商品id获取秒杀商品信息
    LambdaQueryWrapper<SecKillGoods> lambdaQueryWrapper = new QueryWrapper<SecKillGoods>().lambda().eq(SecKillGoods::getGoodsId, goodsVo.getId());
    SecKillGoods seckillGoods = secKillGoodsService.getOne(lambdaQueryWrapper);
    //2.秒杀商品库存-1
    // seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
    // secKillGoodsService.updateById(seckillGoods);

    //1. Mysql在默认的事务隔离级别[REPEATABLE-READ]下
    //2. 执行update语句时,会在事务中锁定要更新的行
    //3. 这样可以防止其它会话在同一行执行update,delete
    LambdaUpdateWrapper<SecKillGoods> updateWrapper = new UpdateWrapper<SecKillGoods>().lambda().setSql("stock_count=stock_count-1")
            .eq(SecKillGoods::getGoodsId, goodsVo.getId())
            .gt(SecKillGoods::getStockCount, 0);
    boolean update = secKillGoodsService.update(updateWrapper);
    //如果更新失败说明已经没有库存了
    if (!update) {
        return null;
    }

    //3.生成普通订单
    Order order = new Order();
    order.setUserId(user.getId());
    order.setGoodsId(goodsVo.getId());
    order.setDeliveryAddrId(0L);
    order.setGoodsName(goodsVo.getGoodsName());
    order.setGoodsCount(1);
    order.setGoodsPrice(seckillGoods.getSeckillPrice());
    order.setOrderChannel(1);
    order.setStatus(0);
    order.setCreateDate(new Date());
    orderMapper.insert(order);
    //4.生成秒杀订单
    SeckillOrder seckillOrder = new SeckillOrder();
    seckillOrder.setGoodsId(goodsVo.getId());
    seckillOrder.setUserId(user.getId());
    seckillOrder.setOrderId(order.getId());
    seckillOrderMapper.insert(seckillOrder);

    //设置秒杀订单的key --> order:用户id:商品id
    redisTemplate.opsForValue().set("order:" + user.getId() + ":" + goodsVo.getId(), seckillOrder);
    return order;
}

说明

  1. 超卖问题处理 前面出现超卖的主要原因在于,更新库存时不具备原子性,这里利用mysql默认的事务隔离级别

[REPEATABLE-READ]在事务中执行update语句获取锁定更新的行,这个机制进行处理

  1. 复购(生成了多个订单)问题处理,则在生成订单之前对库存更新进行判断,如果更新失败则不在生成订单
    //如果更新失败说明已经没有库存了
    if (!update) {
        return null;
    }
  1. 优化复购判断 在生成秒杀订单之后将订单存储到redis中,在判断复购时直接从redis中获取,避免频繁查询数据库,提升执行效率
    //解决重复抢购,直接到redis中获取对应的秒杀订单,如果有则说明已经抢购了,不能继续抢购
    SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
    if (seckillOrder != null) {
        model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
        return "secKillFail";
    }

测试结果

image-20230227224123469

image-20230227224157695

3.3 v3.0 优化秒杀,Redis 预减库存

  1. 前面我们防止超卖 是通过到数据库查询和到数据库抢购,来完成的, 代码如下:

image-20230306215302028

image-20230306215328274

  1. 如果在短时间内,大量抢购冲击 DB, 造成洪峰, 容易压垮数据库
  2. 解决方案: 使用 Redis 完成预减库存,如果没有库存了, 直接返回, 减小对 DB 的压力
  3. 示意图:

image-20230306215452562

代码实现

@Controller
@RequestMapping(value = "/seckill")
public class SeckillController implements InitializingBean {

    @Resource
    private GoodsService goodsService;

    @Resource
    private SeckillOrderService seckillOrderService;

    @Resource
    private OrderService orderService;

    @Resource
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "/doSeckill")
    public String doSeckill(Model model, User user, Long goodsId) {
        System.out.println("-----秒杀 V3.0--------");
        //===================秒杀 v3.0 start =========================
        if (user == null) {
            return "login";
        }
        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        //判断库存
        if (goodsVo.getStockCount() < 1) {
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }
        //解决重复抢购,直接到redis中获取对应的秒杀订单,如果有则说明已经抢购了,不能继续抢购
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (seckillOrder != null) {
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }

        //库存预减,如果在redis中预减库存,发现秒杀商品中已经没有库存了,直接返回
        //从而减少这个方法请求 orderService.seckill防止大量请求打到数据,优化秒杀
        //这个方法具有原子性!!!
        Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);
        //说明这个商品已经没有库存了
        if (decrement < 0) {
            //恢复redis库存为0,对业务没有影响只是为了好看
            redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }

        //抢购
        Order order = orderService.seckill(user, goodsVo);
        if (order == null) {
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }
        model.addAttribute("order", order);
        model.addAttribute("goods", goodsVo);
        return "orderDetail";
        //===================秒杀 v3.0 end... =========================
    }

    //该方法实在类的所有属性都初始化后,自动执行
    //这里将所有秒杀商品的库存量加载到redis中
    @Override
    public void afterPropertiesSet() throws Exception {
        //1.查询所有的秒杀商品
        List<GoodsVo> list = goodsService.findGoodsVo();
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        //遍历list,将秒杀商品的库存量放入到redis中
        //秒杀商品库存量对应的key seckillGoods:商品id
        for (GoodsVo goodsVo : list) {
            redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
        }
    }

}

大体思路:

  1. 项目启动时加载秒杀商品库存到redis中,库存量随着每次启动都会更新

  2. 在进行抢购之前先到redis中进行库存预减,利用redis的decrement具备原子性的特点

  3. 如果redis预减库存小于0则直接返回,避免了多余的请求打到数据库

3.4 v4.0 优化秒杀:加入内存标记,避免总到 Reids 查询库存

  • 需求分析/图解
  1. 如果某个商品库存已经为空了, 我们仍然是到 Redis 去查询的, 还可以进行优化
  2. 解决方案: 给商品进行内存标记, 如果库存为空, 直接返回, 避免总是到 Redis 查询库存
  3. 分析思路-示意图

image-20230306220456230

@Controller
@RequestMapping(value = "/seckill")
public class SeckillController implements InitializingBean {

    @Resource
    private GoodsService goodsService;

    @Resource
    private SeckillOrderService seckillOrderService;

    @Resource
    private OrderService orderService;

    @Resource
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "/doSeckill")
    public String doSeckill(Model model, User user, Long goodsId) {
        System.out.println("-----秒杀 V4.0--------");
        //===================秒杀 v4.0 start =========================
        if (user == null) {
            return "login";
        }
        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        //判断库存
        if (goodsVo.getStockCount() < 1) {
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }
        //解决重复抢购,直接到redis中获取对应的秒杀订单,如果有则说明已经抢购了,不能继续抢购
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (seckillOrder != null) {
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }

        //如果库存为空,避免总是到 reids 去查询库存,给 redis 增加负担(内存标记)
        if (entryStockMap.get(goodsId)) {
            //如果当前这个秒杀商品已经是空库存,则直接返回.
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }

        //库存预减,如果在redis中预减库存,发现秒杀商品中已经没有库存了,直接返回
        //从而减少这个方法请求 orderService.seckill防止大量请求打到数据,优化秒杀
        //这个方法具有原子性!!!
        Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);
        //说明这个商品已经没有库存了
        if (decrement < 0) {
            //这里使用内存标记,避免多次操作 redis, true 表示空库存了.
            entryStockMap.put(goodsId, true);
            //恢复redis库存为0,对业务没有影响只是为了好看
            redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }

        //抢购
        Order order = orderService.seckill(user, goodsVo);
        if (order == null) {
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }
        model.addAttribute("order", order);
        model.addAttribute("goods", goodsVo);
        return "orderDetail";
        //===================秒杀 v4.0 end... =========================
    }

    //该方法实在类的所有属性都初始化后,自动执行
    //这里将所有秒杀商品的库存量加载到redis中
    @Override
    public void afterPropertiesSet() throws Exception {
        //1.查询所有的秒杀商品
        List<GoodsVo> list = goodsService.findGoodsVo();
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        //遍历list,将秒杀商品的库存量放入到redis中
        //秒杀商品库存量对应的key seckillGoods:商品id
        for (GoodsVo goodsVo : list) {
            redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
            //当有库存为 false,当无库存为 true。防止库存没有了,还会到 Redis 进行判断操作
            entryStockMap.put(goodsVo.getId(), false);
        }
    }

}

3.4 v5.0 优化秒杀: 加入消息队列,实现秒杀的异步请求

需求分析/图解

  • 问题分析

前面秒杀, 没有实现异步机制, 是完成下订单后, 再返回的, 当有大并发请求下订单操作时, 数据库来不及响应, 容易造成线程堆积

  • 解决方案
  1. 加入消息队列,实现秒杀的异步请求

  2. 接收到客户端秒杀请求后,服务器立即返回 正在秒杀中…, 有利于流量削峰

  3. 客户端进行轮询秒杀结果, 接收到秒杀结果后,在客户端页面显示即可

  4. 秒杀消息发送设计 SeckillMessage - String

image-20230307223112391

image-20230307223159835

代码实现

秒杀消息类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage {
    private User user;
    private Long goodsId;
}

定义消息队列、交换机

@Configuration
public class RabbitMQSecKillConfig {
    private static final String QUEUE = "seckillQueue";
    private static final String EXCHANGE = "seckillExchange";

    @Bean
    public Queue queue_seckill() {
        return new Queue(QUEUE);
    }

    @Bean
    public TopicExchange topicExchange_seckill() {
        return new TopicExchange(EXCHANGE);
    }

    @Bean
    public Binding binding_seckill() {
        return BindingBuilder.bind(queue_seckill())
                .to(topicExchange_seckill()).with("seckill.#");
    }

}

发送秒杀消息

@Service
@Slf4j
public class MQSenderMessage {

    @Resource
    private RabbitTemplate rabbitTemplate;

    //发送秒杀信息
    public void senderMessage(String message) {
        System.out.println("发送消息了=" + message);
        log.info("发送消息:" + message);
        rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", message);
    }
}

消息接收者

@Service
@Slf4j
public class MQReceiverConsumer {

    @Resource
    private GoodsService goodsService;
    
    @Resource
    private OrderService orderService;

    //下单操作
    @RabbitListener(queues = "seckillQueue")
    public void queue(String message) {
        SeckillMessage seckillMessage = JSONUtil.toBean(message, SeckillMessage.class);
        Long goodId = seckillMessage.getGoodsId();
        User user = seckillMessage.getUser();
        //获取抢购的商品信息
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodId);
        //下单操作
        orderService.seckill(user, goodsVo);
    }

}

SeckillController

@Controller
@RequestMapping(value = "/seckill")
public class SeckillController implements InitializingBean {

    @Resource
    private GoodsService goodsService;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private MQSenderMessage mqSenderMessage;

    //如果某个商品库存已经为空, 则标记到 entryStockMap
    private HashMap<Long, Boolean> entryStockMap = new HashMap<>();

    @RequestMapping(value = "/doSeckill")
    public String doSeckill(Model model, User user, Long goodsId) {
        System.out.println("-----秒杀 V5.0--------");
        //===================秒杀 v5.0 start =========================
        if (user == null) {
            return "login";
        }
        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        //判断库存
        if (goodsVo.getStockCount() < 1) {
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }
        //解决重复抢购,直接到redis中获取对应的秒杀订单,如果有则说明已经抢购了,不能继续抢购
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (seckillOrder != null) {
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }

        //如果库存为空,避免总是到 reids 去查询库存,给 redis 增加负担(内存标记)
        if (entryStockMap.get(goodsId)) {
            //如果当前这个秒杀商品已经是空库存,则直接返回.
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }

        //库存预减,如果在redis中预减库存,发现秒杀商品中已经没有库存了,直接返回
        //从而减少这个方法请求 orderService.seckill防止大量请求打到数据,优化秒杀
        //这个方法具有原子性!!!
        Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);
        //说明这个商品已经没有库存了
        if (decrement < 0) {
            //这里使用内存标记,避免多次操作 redis, true 表示空库存了.
            entryStockMap.put(goodsId, true);
            //恢复redis库存为0,对业务没有影响只是为了好看
            redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }

        //抢购
        // Order order = orderService.seckill(user, goodsVo);
        // if (order == null) {
        //     model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
        //     return "secKillFail";
        // }
        // model.addAttribute("order", order);
        // model.addAttribute("goods", goodsVo);
        //抢购, 向 MQ 发消息, 因为不知道是否成功, 客户端需要轮询
        //errmsg 为排队中.... , 暂时返回 "secKillFaill" 页面
        SeckillMessage seckillMessage =
                new SeckillMessage(user, goodsId);
        mqSenderMessage.senderMessage
                (JSONUtil.toJsonStr(seckillMessage));
        model.addAttribute("errmsg", "排队中.....");
        return "orderDetail";
        //===================秒杀 v5.0 end... =========================
    }


    //该方法实在类的所有属性都初始化后,自动执行
    //这里将所有秒杀商品的库存量加载到redis中
    @Override
    public void afterPropertiesSet() throws Exception {
        //1.查询所有的秒杀商品
        List<GoodsVo> list = goodsService.findGoodsVo();
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        //遍历list,将秒杀商品的库存量放入到redis中
        //秒杀商品库存量对应的key seckillGoods:商品id
        for (GoodsVo goodsVo : list) {
            redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
            //当有库存为 false,当无库存为 true。防止库存没有了,还会到 Redis 进行判断操作
            entryStockMap.put(goodsVo.getId(), false);
        }
    }

}
  • 客户端轮询秒杀结果-思路分析示意图

image-20230311122052931

image-20230311122517345

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/403953.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

麒麟服务器V10 版本 安装 Anaconda教程,也就是安装Python环境的教程(亲测有效)

目录1 Anaconda 是什么2 安装1 Anaconda 是什么 你可以理解为一个软件&#xff0c;和QQ一样的软件&#xff0c;你安装之后&#xff0c;里面就有naconda包括Conda、Python以及一大堆安装好的工具包&#xff0c;比如&#xff1a;numpy、pandas等 1&#xff09;包含conda&#x…

【C++学习】类和对象(上)

前言&#xff1a; 由于之前电脑“嗝屁”了&#xff0c;导致这之前一直没有更新博客&#xff0c;今天才拿到电脑&#xff0c;在这里说声抱歉。接下来就进入今天的学习&#xff0c;在之前我们已经对【C】进行了初步的认识&#xff0c;有了之前的知识铺垫&#xff0c;今天我们将来…

初识BFC

初识BFC 先说如何开启BFC&#xff1a; 1.设置display属性&#xff1a;inline-block&#xff0c;flex&#xff0c;grid 2.设置定位属性&#xff1a;absolute&#xff0c;fixed 3.设置overflow属性&#xff1a;hidden&#xff0c;auto&#xff0c;scroll 4.设置浮动&#xf…

英雄算法学习路线

文章目录零、自我介绍一、关于拜师二、关于编程语言三、算法学习路线1、算法集训1&#xff09;九日集训2&#xff09;每月算法集训2、算法专栏3、算法总包四、英雄算法联盟1、英雄算法联盟是什么&#xff1f;2、如何加入英雄算法联盟&#xff1f;3、为何会有英雄算法联盟&#…

Linux系统安装mysql(rpm版)

目录 Linux系统安装mysql&#xff08;rpm版&#xff09; 1、检测当前系统中是否安装MySQL数据库 2、将mysql安装包上传到Linux并解压 3、按照顺序安装rpm软件包 4、启动mysql 5、设置开机自启 6、查看已启动的服务 7、查看临时密码 8、登录mysql&#xff0c;输入临时密…

C++ STL学习之【vector的使用】

✨个人主页&#xff1a; Yohifo &#x1f389;所属专栏&#xff1a; C修行之路 &#x1f38a;每篇一句&#xff1a; 图片来源 The power of imagination makes us infinite. 想象力的力量使我们无限。 文章目录&#x1f4d8;前言&#x1f4d8;正文1、默认成员函数1.1、默认构造…

STM32之SPI

SPISPI介绍SPI是串行外设接口(Serial Peripherallnterface)的缩写&#xff0c;是一种高速的&#xff0c;全双工&#xff0c;同步的通信总线&#xff0c;并且在芯片的管脚上只占用四根线&#xff0c;节约了芯片的管脚&#xff0c;同时为PCB的布局上节省空间&#xff0c;提供方便…

蓝桥杯嵌入式(G4系列):定时器捕获

前言&#xff1a; 定时器的三大功能还剩下最后一个捕获&#xff0c;而这在蓝桥杯嵌入式开发板上也有555定时器可以作为信号发生器供定时器来测量。 原理图部分&#xff1a; 开发板上集成了两个555定时器&#xff0c;一个通过跳线帽跟PA15相连&#xff0c;最终接到了旋钮R40上&…

STM32F103CubeMX定时器

前言定时器作为最重要的内容之一&#xff0c;是每一位嵌入式软件工程师必备的能力。STM32F103的定时器是非常强大的。1&#xff0c;他可以用于精准定时&#xff0c;当成延时函数来使用。不过个人不建议这么使用&#xff0c;因为定时器很强大&#xff0c;这么搞太浪费了。如果想…

Zookeeper的Java API操作

Zookeeper的Java API操作一、先启动Zookeeper集群二、IDEA 环境搭建三、创建子节点四、获取子节点并监听节点变化五、判断 Znode 是否存在六、Watcher工作流程一、先启动Zookeeper集群 二、IDEA 环境搭建 1.创建一个Maven工程&#xff1a;ZookeeperProject 2.在pom.xml文件添…

ARM uboot 的移植4 -从 uboot 官方标准uboot开始移植

一、添加DDR初始化1 1、分析下一步的移植路线 (1) cpu_init_crit 函数成功初始化串口、时钟后&#xff0c;转入 _main 函数&#xff0c;函数在 arch/arm/lib/crt0.S 文件中。 (2) 在 crt0.S 中首先设置栈&#xff0c;将 sp 指向 DDR 中的栈地址&#xff1b; #if defined(CONF…

CNCF x Alibaba云原生技术公开课 【重要】第九章 应用存储和持久化数据卷:核心知识

1、Pod Volumes 场景 同一个pod中的某个容器异常退出&#xff0c;kubelet重新拉起来&#xff0c;保证容器之前产生数据没丢同一个pod的多个容器共享数据 常见类型 本地存储&#xff0c;常用的有 emptydir/hostpath&#xff1b;网络存储&#xff1a;网络存储当前的实现方式有两…

2021年我国半导体分立器件市场规模已达3037亿元,国内功率半导体需求持续快速增长

半导体分立器件是由单个半导体晶体管构成的具有独立、完整功能的器件。例如&#xff1a;二极管、三极管、双极型功率晶体管(GTR)、晶闸管(可控硅)、场效应晶体管(结型场效应晶体管、MOSFET)、IGBT、IGCT、发光二极管、敏感器件等。半导体分立器件制造&#xff0c;指单个的半导体…

proteus I2C Debugger 查看 AT24C02写入读取

I2C Debugger仪器&#xff0c;在仿真调试期中&#xff0c;该仪器可以显示I2C数据传送时间、S&#xff08;START状态&#xff09;、Sr(ReStart状态&#xff09;、A&#xff08;Ask响应&#xff09;、N &#xff08;No ask状态&#xff09;、P&#xff08;Stop状态&#xff09;、…

中值滤波+Matlab仿真+频域响应分析

中值滤波 文章目录中值滤波理解中值滤波的过程Matlab 实现实际应用频域分析中值滤波是一种滤波算法&#xff0c;其目的是去除信号中的噪声&#xff0c;而不会对信号本身造成太大的影响。它的原理非常简单&#xff1a;对于一个给定的窗口大小&#xff0c;将窗口内的数值排序&…

【C++进阶】四、红黑树(三)

目录 一、红黑树的概念 二、红黑树的性质 三、红黑树节点的定义 四、红黑树的插入 五、红黑树的验证 六、红黑树与AVL树的比较 七、完整代码 一、红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可…

嵌入式安防监控项目——html框架分析和环境信息刷新到网页

目录 一、html控制LED 二、模拟数据上传到html 一、html控制LED 简单来说就是html给boa服务器发了一个控制指令信息&#xff0c;然后boa转发给cgi进程&#xff0c;cgi通过消息队列和主进程通信。主进程再去启动LED子线程。 这是老师给的工程。 以前学32都有这工具那工具来管…

导航技术调研(CSDN_0023_20221217)

文章编号&#xff1a;CSDN_0023_20221217 目录 1. 惯性导航 2. 组合导航技术 3. 卡尔曼滤波 1. 惯性导航 惯性导航系统(INS-Inertial Navigation System)是上个世纪初发展起来的。惯性导航是一种先进的导航方法&#xff0c;但实现导航定位的原理却非常简单&#xff0c;它是…

RHCSA-用户和组管理和文件系统权限(3.11)

目录 用户&#xff08;UID&#xff09; 用户类别&#xff08;UID&#xff09;&#xff1a; 用户的增删改查&#xff1a; 修改用户密码&#xff1a; 查看用户是否存在&#xff1a; 组&#xff08;GID&#xff09; 组的增删改查&#xff1a; 设置组密码&#xff1a; 用户…

idea集成GitHub

设置 GitHub 账号绑定账号有两种方式&#xff1a;1. 通过授权登录2.如果上述登录不成功&#xff0c;用Token口令的方式登录&#xff0c;口令在github账号哪里生成&#xff0c;点击settings --->Developer settings --->pwrsonal access tokens ----> 复制口令到idea 口…