高并发秒杀使用RabbitMQ的优化思路

news2025/4/23 12:00:05

高并发秒杀使用RabbitMQ的优化思路

  • 一、判断是否重复抢购(防止一人多次秒杀)的逻辑
    • 1. 整体逻辑代码
    • 2. 原始判断重复抢购的方式:
    • 3. 后来优化为什么用 Redis 判断?
  • 二、高并发下优化过的秒杀逻辑
    • 1.秒杀核心逻辑(请求入口)
    • 2.系统初始化逻辑(项目启动时调用)
    • 3. 整体流程
  • 三、 RabbitMQ 秒杀消息的发送与消费逻辑
    • 1. `RabbitMQConfig.java`:配置 RabbitMQ 消息队列
    • 2. `MQSender.java`:发送秒杀消息
    • 3. `MQReceiver.java`:接收秒杀消息并处理
    • 4. 秒杀消息处理完整流程总结
    • 5. 重复判断问题
  • 四、 秒杀系统异步下单+轮询查询结果
    • 1.整体流程图
    • 2. `/result` 轮询接口逻辑详解
    • 3. 对应的 `getResult()` 方法逻辑:
    • 4. 为什么要这样设计?
  • 五、Redis分布式锁的原子性操作
    • 缘起:
      • 1. 问题场景:高并发秒杀下的“扣库存”
      • 2. 核心问题:这不是一个**原子操作**
        • 多线程并发问题示意:
        • 结果:
      • 3. 根本原因:Redis 这些操作不是“原子性的”
      • 4. 解决思路:加锁(分布式锁)
        • 效果:
      • 5. 再进一步:为什么普通锁也不够,还要加唯一值 + Lua 脚本?
    • 第一步:最基础的锁实现
      • 目的:
      • 存在的问题:
    • 第二步:加上过期时间(自动释放锁)
      • 改进点:
      • 新的问题:
    • 第三步:引入唯一标识防止误删(UUID)
      • 改进点:
      • 新的问题:
    • 第四步:用 Lua 脚本原子释放锁
      • 改进点:
    • 总结

一、判断是否重复抢购(防止一人多次秒杀)的逻辑

1. 整体逻辑代码

// 查询指定商品的详细信息,包括秒杀价格、库存等
GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);

// 判断商品库存是否充足(即是否还有剩余可秒杀的数量)
if (goods.getStockCount() < 1) {
    // 库存不足,返回秒杀失败,提示库存为空
    return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}

// 判断当前用户是否已经秒杀过该商品(防止重复抢购)
// 注释掉的是原来的数据库方式判断:
// SeckillOrder seckillOrder = seckillOrderService.getOne(new 
//     QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));

// 使用 Redis 判断是否已经下过秒杀订单
// 拼接 Redis key:order:用户id:商品id
String seckillOrderJson = (String) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);

// 判断 Redis 中是否存在该 key 的值,说明该用户已经抢购过
if (!StringUtils.isEmpty(seckillOrderJson)) {
    // 存在记录,说明重复抢购,返回错误提示
    return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}

// 正常进入下单流程,调用秒杀下单服务
Order order = orderService.seckill(user, goods);

// 如果下单成功,返回成功响应以及订单对象
if (null != order) {
    return RespBean.success(order);
}

这段代码整体逻辑顺序如下:

  1. 获取商品详情;
  2. 判断库存;
  3. 判断是否重复下单(现在是用 Redis);
  4. 调用下单逻辑;
  5. 返回结果。

2. 原始判断重复抢购的方式:

// 从数据库中查找是否已经存在该用户对该商品的秒杀订单
SeckillOrder seckillOrder = seckillOrderService.getOne(
    new QueryWrapper<SeckillOrder>()
        .eq("user_id", user.getId())
        .eq("goods_id", goodsId)
);
new QueryWrapper<SeckillOrder>() // 创建一个查询构造器,用于构造 SeckillOrder 表的查询条件
    .eq("user_id", user.getId()) // 添加查询条件:字段 user_id 等于当前用户的 ID(即查询该用户的记录)
    .eq("goods_id", goodsId)     // 添加查询条件:字段 goods_id 等于当前商品的 ID(即查询该商品的记录)

这段代码等价于 SQL 中的:

SELECT * FROM seckill_order 
WHERE user_id = 当前用户ID AND goods_id = 当前商品ID;

从秒杀订单表中查询 user_id 等于当前用户,且 goods_id 等于当前商品 的记录。

  • 逻辑解释:

    • SeckillOrder 表是秒杀订单表,设置了唯一索引,同一个用户,对同一件商品,只能有一条秒杀订单记录。
CREATE TABLE seckill_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    goods_id BIGINT NOT NULL,
    order_id BIGINT NOT NULL,
    -- 其他字段 ...
    UNIQUE KEY uniq_user_goods (user_id, goods_id)
);
  • 如果 seckillOrder != null,就说明用户已经抢购过:
if (seckillOrder != null) {
    return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}

3. 后来优化为什么用 Redis 判断?

String seckillOrderJson = (String) redisTemplate.opsForValue()
    .get("order:" + user.getId() + ":" + goodsId);
if (!StringUtils.isEmpty(seckillOrderJson)) {
    return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
  • 优点

    • 性能更高:不查数据库,改查 Redis,速度更快,减轻数据库压力。
    • 适合高并发场景:秒杀场景下请求量大,Redis 更适合高并发判断。

二、高并发下优化过的秒杀逻辑

1.秒杀核心逻辑(请求入口)

// 获取 Redis 中的操作对象,用于字符串类型操作
ValueOperations valueOperations = redisTemplate.opsForValue();

// -------- 判断是否重复抢购 --------
// 从 Redis 中获取该用户是否已经抢购过该商品
String seckillOrderJson = (String) valueOperations.get("order:" + user.getId() + ":" + goodsId);

// 如果已经存在该用户对该商品的订单,说明是重复抢购
if (!StringUtils.isEmpty(seckillOrderJson)) {
    return RespBean.error(RespBeanEnum.REPEATE_ERROR); // 返回“重复秒杀”错误
}

// -------- 内存标记减少 Redis 访问 --------
// 如果内存中的标记已经说明该商品没有库存了,直接返回,减少对 Redis 的访问
if (EmptyStockMap.get(goodsId)) {
    return RespBean.error(RespBeanEnum.EMPTY_STOCK); // 返回“库存为空”错误
}

// -------- 预减库存(Redis 预扣减) --------
// 对 Redis 中的商品库存执行递减操作
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);

// 如果库存扣减后小于 0,说明库存已被抢光
if (stock < 0) {
    // 设置内存标记,后续请求就不再访问 Redis 了
    EmptyStockMap.put(goodsId, true);

    // 回滚 Redis 中的库存(因为刚才减了一次)
    valueOperations.increment("seckillGoods:" + goodsId);

    return RespBean.error(RespBeanEnum.EMPTY_STOCK); // 返回“库存为空”错误
}

// -------- 请求入队(异步下单) --------
// 创建秒杀消息对象,封装用户和商品信息
SeckillMessage message = new SeckillMessage(user, goodsId);

// 发送消息到 RabbitMQ 队列,让后端异步去处理下单逻辑
mqSender.sendsecKillMessage(JsonUtil.object2JsonStr(message));

// 秒杀请求排队中,立即返回成功(前端可以轮询查询是否下单成功)
return RespBean.success(0);

2.系统初始化逻辑(项目启动时调用)

// 实现 InitializingBean 接口的 afterPropertiesSet 方法,在 Spring 初始化 Bean 后执行
@Override
public void afterPropertiesSet() throws Exception {
    // 查询所有参与秒杀的商品列表
    List<GoodsVo> list = goodsService.findGoodsVo();

    // 如果商品列表为空,直接返回
    if (CollectionUtils.isEmpty(list)) {
        return;
    }

    // 遍历每个商品,将库存数量加载到 Redis,同时初始化内存标记为“有库存”
    list.forEach(goodsVo -> {
        // Redis 中设置商品库存,key 是 seckillGoods:商品ID,value 是库存数量
        redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(),
                                        goodsVo.getStockCount());

        // 内存中标记该商品有库存(false 表示“未被标记为无库存”)
        EmptyStockMap.put(goodsVo.getId(), false);
    });
}


3. 整体流程

阶段技术作用
重复抢购校验Redis + 用户ID-商品ID 键高效判断是否已经秒杀过
库存控制Redis decrement避免并发超卖
内存标记EmptyStockMap避免频繁访问 Redis
异步处理RabbitMQ + 秒杀消息对象将核心下单操作交由后端异步处理,减轻主线程压力
初始化Redis 预加载提前加载秒杀商品库存,提升响应速度

三、 RabbitMQ 秒杀消息的发送与消费逻辑

1. RabbitMQConfig.java:配置 RabbitMQ 消息队列

package com.xxxxx.seckill.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * RabbitMQ 配置类
 * 配置队列、交换机和绑定关系
 * 用于秒杀系统的消息异步处理
 */
@Configuration
public class RabbitMQConfig {
    // 定义队列名称常量
    private static final String QUEUE = "seckillQueue";
    // 定义交换机名称常量
    private static final String EXCHANGE = "seckillExchange";
        /**
     * 定义一个名为 seckillQueue 的队列
     * @return 队列对象
     */
    @Bean
    public Queue queue(){
        return new Queue(QUEUE);
    }
     /**
     * 将队列与交换机进行绑定,并设置路由键为 seckill.#
     * 意味着所有以 seckill. 开头的消息都会被路由到 seckillQueue 队列中
     */
    @Bean
    public Binding binding01(){
        return BindingBuilder
                .bind(queue())                // 绑定队列
                .to(topicExchange())          // 指定交换机
                .with("seckill.#");           // 路由键匹配规则:以 seckill. 开头的所有消息
    }
}

2. MQSender.java:发送秒杀消息

package com.xxxxx.seckill.rabbitmq;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 消息发送者(将秒杀请求异步发送到 RabbitMQ)
 */
@Service
@Slf4j
public class MQSender {

    // 注入 RabbitTemplate,用于操作 RabbitMQ
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送秒杀消息
     * @param message 消息体(通常是包含用户和商品ID的 JSON 字符串)
     */
    public void sendsecKillMessage(String message) {
        log.info("发送消息:" + message);  // 打印日志,便于调试
        // 发送消息到交换机 seckillExchange,使用 routingKey 为 seckill.msg
        rabbitTemplate.convertAndSend("seckillExchange", "seckill.msg", message);
    }
}

3. MQReceiver.java:接收秒杀消息并处理

package com.xxxxx.seckill.rabbitmq;

import com.xxxxx.seckill.pojo.User;
import com.xxxxx.seckill.service.IGoodsService;
import com.xxxxx.seckill.service.IOrderService;
import com.xxxxx.seckill.util.JsonUtil;
import com.xxxxx.seckill.vo.GoodsVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

这些是基本的包导入,含业务服务类、工具类和 Redis 组件。

/**
 * 消息接收者(从 RabbitMQ 获取秒杀请求并处理)
 */
@Service
@Slf4j
public class MQReceiver {

    @Autowired
    private IGoodsService goodsService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IOrderService orderService;

    /**
     * 消费者监听 seckillQueue 队列
     * 接收到消息后开始处理秒杀逻辑
     */
    @RabbitListener(queues = "seckillQueue")
    public void receive(String msg) {
        log.info("QUEUE接受消息:" + msg); // 打印日志

        // 将 JSON 字符串反序列化成 SeckillMessage 对象
        SeckillMessage message = JsonUtil.jsonStr2Object(msg, SeckillMessage.class);

        // 从消息中提取商品ID和用户信息
        Long goodsId = message.getGoodsId();
        User user = message.getUser();

        // 查询商品详情(包括秒杀库存等)
        GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);

        // ------- 判断库存是否足够 -------
        if (goods.getStockCount() < 1) {
            return; // 库存不足,直接返回,不再继续处理
        }

        // ------- 判断是否重复秒杀 -------
        // 使用 Redis 判断该用户是否已抢购该商品(Redis中有记录则表示已经下单)
        String seckillOrderJson = (String)
                redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (!StringUtils.isEmpty(seckillOrderJson)) {
            return; // 已经秒杀过了,直接返回
        }

        // ------- 执行秒杀下单逻辑 -------
        // 调用订单服务,完成订单生成、库存扣减等
        orderService.seckill(user, goods);
    }
}

4. 秒杀消息处理完整流程总结

步骤说明
1. 前端点击“秒杀”按钮请求发送到后台秒杀接口
2. 后台进行校验包括是否重复抢购、库存校验、内存标记
3. 校验通过后发送消息使用 MQSender 发送消息到 RabbitMQ
4. 消费端监听 seckillQueue使用 @RabbitListener 自动接收消息
5. 反序列化消息转换为 SeckillMessage 对象
6. 查询商品信息获取库存
7. 再次校验是否重复秒杀、库存是否足够
8. 执行下单逻辑调用 orderService.seckill() 进行下单入库、更新 Redis 等操作

5. 重复判断问题

已经在接口层(controller/service)对库存是否足够和是否重复秒杀做了一次判断,为什么在 MQReceiver.java 里还要再判断一遍呢?

答案可以用一句话总结:

因为消息队列是异步的,接口层的判断并不能保证最终数据一致性。真正的“抢购成功”必须由消息消费方进行最终确认。

  • 第一次判断(接口层 秒杀接口 中):
// 1. 判断是否重复秒杀(Redis 中存在这个用户和商品的订单)
if (!StringUtils.isEmpty(seckillOrderJson)) {
    return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}

// 2. 判断库存是否为 0(Redis 预减库存)
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
if (stock < 0) {
    EmptyStockMap.put(goodsId,true);
    valueOperations.increment("seckillGoods:" + goodsId);
    return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}

// 3. 通过 RabbitMQ 异步下单
mqSender.sendsecKillMessage(JsonUtil.object2JsonStr(message));

这个阶段主要是为了快速响应用户请求、限流、预拦截非法操作。因为秒杀高并发,不能所有请求都进入数据库,先通过 Redis 做一轮筛选。


  • 第二次判断(消费方 MQReceiver.java 中):
// 1. 获取商品库存信息(查数据库)
if (goods.getStockCount() < 1) {
    return;
}

// 2. 再次判断是否已经秒杀(Redis 或数据库确认)
String seckillOrderJson = (String)
    redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (!StringUtils.isEmpty(seckillOrderJson)) {
    return;
}

  • 为什么还要再次判断?

    • 数据最终一致性的保障(兜底逻辑)

      • 用户发起请求时可能网络延迟、并发穿透、Redis 短暂未同步等问题,导致多个请求都通过了前端判断
      • 如果消费方不再次判断,就可能出现超卖重复下单
    • 防止 Redis 与数据库数据不一致

      • Redis 是缓存,最终写入的订单和库存数据必须以数据库为准
      • Redis 中库存可能出现错误(例如手动清空 Redis 缓存后重启服务),但数据库是强一致的。
    • 避免“多次秒杀”绕过逻辑

      • 如果用户用不同终端 / IP 并发请求,有可能绕过前端检查,甚至模拟请求。
      • 所以最后还是得由消费方从数据库或 Redis再次校验。

  • 总结:两层判断是为了兼顾性能 + 数据安全
层级处理位置作用缺点优点
第一次接口层(Controller / Service)快速判断,提高性能,减轻 MQ 和数据库压力数据不一定可靠响应快,限流效果好
第二次MQReceiver 消费者端最终判断是否成功抢购响应慢(异步)保证数据一致性,防止超卖和重复秒杀

四、 秒杀系统异步下单+轮询查询结果

1.整体流程图

   用户点击「秒杀」按钮
           |
    发起 /doSeckill 请求(通常是 POST)
           |
     秒杀服务判断幂等、库存、入队
           |
   秒杀消息被投递到 MQ(如RabbitMQ)
           |
       ---异步处理开始---
           |
       MQReceiver 消费消息
           |
     判断库存是否充足、是否重复秒杀
           |
     创建订单 & 秒杀订单 & 写Redis标记
           |
       ---异步处理结束---
           |
  客户端开始定时轮询 /result 接口(GET)
           |
     ISeckillOrderService.getResult(user, goodsId)
           |
     Redis中判断是否库存为空 or 查询订单记录
           |
       返回三种状态:
        ✔️ 订单ID:成功
        ❌ -1:失败(库存为空)
        ⏳ 0:排队中(异步线程尚未处理完)

2. /result 轮询接口逻辑详解

你提供的 /result Controller:

@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
public RespBean getResult(User user, Long goodsId) {
    if (user == null) {
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
    }
    Long orderId = seckillOrderService.getResult(user, goodsId);
    return RespBean.success(orderId);
}

3. 对应的 getResult() 方法逻辑:

@Override
public Long getResult(User user, Long goodsId) {
    // 1. 从数据库中查询是否已经生成秒杀订单
    SeckillOrder seckillOrder = seckillOrderMapper.selectOne(
        new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId)
    );

    if (seckillOrder != null) {
        return seckillOrder.getId(); // 秒杀成功,返回订单ID
    }

    // 2. 如果Redis中标记了库存为空,说明秒杀失败
    if (redisTemplate.hasKey("isStockEmpty:" + goodsId)) {
        return -1L; // 秒杀失败
    }

    // 3. 否则仍在排队中
    return 0L;
}

4. 为什么要这样设计?

  1. 异步处理下单:减少数据库压力,防止并发写入造成阻塞。
  2. 轮询查询结果:前端不断请求 /result 来获得是否秒杀成功。
  3. Redis做标记
    • isStockEmpty:goodsId:快速失败标记,防止浪费时间排队。
    • order:userId:goodsId:避免重复秒杀,保证幂等性。
  4. 高并发友好:因为下单操作是异步的,客户端轮询不会阻塞主线程,也减少数据库压力。

五、Redis分布式锁的原子性操作

缘起:

上面代码实际演示会发现Redis的库存有问题,原因在于Redis没有做到原子性。

1. 问题场景:高并发秒杀下的“扣库存”

假设你在做一个“秒杀”活动,商品库存是 10,使用 Redis 存储库存数量:

set stock 10

每当一个用户下单时,就会执行如下操作:

int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
    redisTemplate.opsForValue().set("stock", stock - 1);
}

2. 核心问题:这不是一个原子操作

上面代码是三个步骤:

  1. 读取库存stock = 10
  2. 判断是否大于0
  3. 更新库存stock = 9
多线程并发问题示意:

假设两个线程 A 和 B 几乎同时执行:

时间顺序线程 A线程 B
T1读取 stock=10
T2读取 stock=10
T3判断 >0判断 >0
T4写入 stock=9写入 stock=9
结果:

虽然来了两个用户,正确的逻辑应该库存变为 8,但实际却被覆盖成了 9,相当于少扣了一次库存(出现超卖/重复卖的问题)。


3. 根本原因:Redis 这些操作不是“原子性的”

Redis 单个命令是原子性的,但你把多个命令组合起来执行时(如 get + if + set)就不是原子操作了

也就是说:

多个命令之间线程是可以插队的,这就导致了并发安全问题。


4. 解决思路:加锁(分布式锁)

为了解决这个并发问题,我们引入锁机制

if (get lock成功) {
    // 执行:get stock -> check -> set stock
    // 释放锁
}
效果:
  • 同一时间只有一个线程能进来执行扣库存
  • 其他线程只能等或者返回“库存紧张,请稍后再试”

5. 再进一步:为什么普通锁也不够,还要加唯一值 + Lua 脚本?

因为如下问题:

  • 如果执行慢,锁自动过期,其他线程进来了,但旧线程还在执行
  • 如果释放锁不判断是否自己加的,可能误删别人的锁

所以最终需要用:

  • 唯一标识(UUID)绑定线程
  • Lua 脚本保证删除锁是原子操作

第一步:最基础的锁实现

Boolean isLock = valueOperations.setIfAbsent("k1", "v1");

目的:

  • 使用 Redis 的 SETNX(set if not exists)机制实现分布式锁。
  • 如果返回 true,就认为加锁成功,进入临界区,操作完后手动 del 删除锁。

存在的问题:

  • 没有过期时间,如果线程意外挂掉(如异常、宕机)就会造成死锁
  • 没有考虑并发线程之间的唯一标识,锁可能会被误删

第二步:加上过期时间(自动释放锁)

Boolean isLock = valueOperations.setIfAbsent("k1", "v1", 5, TimeUnit.SECONDS);

改进点:

  • 给锁设置了5秒过期时间,防止程序异常导致锁无法释放。
  • 这个使用了 RedisTemplate.setIfAbsent(K key, V value, long timeout, TimeUnit unit) 方法。

新的问题:

  • 如果业务处理时间超过5秒,锁就会提前过期被释放,导致下一个线程以为可以进来,造成多个线程并发执行临界区代码,违背加锁初衷。

第三步:引入唯一标识防止误删(UUID)

String uuid = UUID.randomUUID().toString();
Boolean isLock = redisTemplate.opsForValue().setIfAbsent("k1", uuid, 5, TimeUnit.SECONDS);
...
// 释放锁前先判断value
String value = (String) redisTemplate.opsForValue().get("k1");
if (uuid.equals(value)) {
    redisTemplate.delete("k1");
}

改进点:

  • 给每个线程生成一个 唯一标识(UUID),只允许加锁的线程自己释放锁
  • 解决了“线程A释放线程B锁”的问题。

新的问题:

  • get + delete 是两个独立操作,中间可能有线程切换,仍然有并发安全问题,无法保证原子性!

第四步:用 Lua 脚本原子释放锁

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(redisScript, Collections.singletonList("k1"), uuid);

改进点:

  • 使用 Lua 脚本将“判断 + 删除”打包成一个 Redis 原子操作。
  • Redis 保证 Lua 脚本执行期间不会有其他命令插入,彻底解决误删问题。
  • 实现了真正意义上的线程安全和可靠释放锁。

总结

阶段代码关键点解决问题遗留问题
1️⃣ 初始锁 setIfAbsent(k, v)实现基本分布式锁没有过期时间,可能死锁
2️⃣ 加过期时间 setIfAbsent(k, v, timeout)防止死锁业务执行慢时锁可能提前释放
3️⃣ 加唯一值(UUID) + get + delete防止误删他人锁get+delete 非原子
4️⃣ Lua 脚本判断+删除完整原子释放锁,最终完善版本基础 Redis 实现,后续可封装成工具

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

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

相关文章

【LeetCode】嚼烂热题100【持续更新】

2、字母异位词分组 方法一&#xff1a;排序哈希表 思路&#xff1a;对每个字符串排序&#xff0c;排序后的字符串作为键插入到哈希表中&#xff0c;值为List<String>形式存储单词原型&#xff0c;键为排序后的字符串。 Map<String, List<String>> m new Ha…

赛灵思 XC7K325T-2FFG900I FPGA Xilinx Kintex‑7

XC7K325T-2FFG900I 是 Xilinx Kintex‑7 系列中一款工业级 (I) 高性能 FPGA&#xff0c;基于 28 nm HKMG HPL 工艺制程&#xff0c;核心电压标称 1.0 V&#xff0c;I/O 电压可在 0.97 V–1.03 V 之间灵活配置&#xff0c;并可在 –40 C 至 100 C 温度范围内稳定运行。该器件提供…

k8s-1.28.10 安装metrics-server

1.简介 Metrics Server是一个集群范围的资源使用情况的数据聚合器。作为一个应用部署在集群中。Metric server从每个节点上KubeletAPI收集指标&#xff0c;通过Kubernetes聚合器注册在Master APIServer中。为集群提供Node、Pods资源利用率指标。 2.下载yaml文件 wget https:/…

基于外部中中断机制,实现以下功能: 1.按键1,按下和释放后,点亮LED 2.按键2,按下和释放后,熄灭LED 3.按键3,按下和释放后,使得LED闪烁

题目&#xff1a; 参照外部中断的原理和代码示例,再结合之前已经实现的按键切换LED状态的实验&#xff0c;用外部中断改进其实现。 请自行参考文档《中断》当中&#xff0c;有关按键切换LED状态的内容, 自行连接电路图&#xff0c;基于外部中断机制&#xff0c;实现以下功能&am…

【我的创作纪念日】 --- 与CSDN走过的第365天

个人主页&#xff1a;夜晚中的人海 不积跬步&#xff0c;无以至千里&#xff1b;不积小流&#xff0c;无以成江海。-《荀子》 文章目录 &#x1f389;一、机缘&#x1f680;二、收获&#x1f3a1;三、 日常⭐四、成就&#x1f3e0;五、憧憬 &#x1f389;一、机缘 光阴似箭&am…

鸿蒙生态新利器:华为ArkUI-X混合开发框架深度解析

鸿蒙生态新利器&#xff1a;华为ArkUI-X混合开发框架深度解析 作者&#xff1a;王老汉 | 鸿蒙生态开发者 | 2025年4月 &#x1f4e2; 前言&#xff1a;开发者们的新机遇 各位鸿蒙开发者朋友们&#xff0c;是否还在为多平台开发重复造轮子而苦恼&#xff1f;今天给大家介绍一位…

‌信号调制与解调技术基础解析

调制解调技术是通信系统中实现基带信号与高频载波信号相互转换的主要技术&#xff0c;通过调整信号特性使其适应不同信道环境&#xff0c;保障信息传输的效率和可靠性。 调制与解调的基本概念 调制&#xff08;Modulation&#xff09;‌ 将低频基带信号&#xff08;如语音或数…

【扫描件批量改名】批量识别扫描件PDF指定区域内容,用识别的内容修改PDF文件名,基于C++和腾讯OCR的实现方案,超详细

批量识别扫描件PDF指定区域内容并重命名文件方案 应用场景 本方案适用于以下场景: 企业档案数字化管理:批量处理扫描的合同、发票等文件,按内容自动分类命名财务票据处理:自动识别票据上的关键信息(如发票号码、日期)用于归档医疗记录管理:从扫描的检查报告中提取患者I…

序列决策问题(Sequential Decision-Making Problem)

序列决策问题&#xff08;Sequential Decision-Making Problem&#xff09;是强化学习&#xff08;Reinforcement Learning, RL&#xff09;的核心研究内容&#xff0c;其核心思想是&#xff1a;​​智能体&#xff08;Agent&#xff09;需要在连续的时间步骤中&#xff0c;通过…

L2-1、打造稳定可控的 AI 输出 —— Prompt 模板与格式控制

一、为什么需要 Prompt 模板&#xff1f; 在与 AI 模型交互时&#xff0c;我们经常会遇到输出不稳定、格式混乱的问题。Prompt 模板帮助我们解决这些问题&#xff0c;通过结构化的输入指令来获得可预测且一致的输出结果。 模板的作用主要体现在&#xff1a; 固定输出格式&am…

LLM中什么是模板定义、提示工程和文档处理链

LLM中什么是模板定义、提示工程和文档处理链 定义提示模板(prompt_template):prompt_template = """Use the following pieces of context to answer the question at the end. If you dont know the answer, just say that you dont know, dont try to make…

密码学(二)流密码

2.1流密码的基本概念 流密码的基本思想是利用密钥 k 产生一个密钥流...&#xff0c;并使用如下规则对明文串 ... 加密&#xff1a;。密钥流由密钥流发生器产生&#xff1a; &#xff0c;这里是加密器中的记忆元件&#xff08;存储器&#xff09;在时刻 i 的状态&#xff0c…

力扣第446场周赛

有事没赶上, 赛后模拟了一下, 分享一下我的解题思路和做题感受 1.执行指令后的得分 题目链接如下&#xff1a;力扣 给你两个数组&#xff1a;instructions 和 values&#xff0c;数组的长度均为 n。 你需要根据以下规则模拟一个过程&#xff1a; 从下标 i 0 的第一个指令开…

OpenCV中的透视变换方法详解

文章目录 引言1. 什么是透视变换2. 透视变换的数学原理3. OpenCV中的透视变换代码实现3.1 首先定义四个函数 3.1.1 cv_show() 函数 3.1.2 def resize() 函数 3.1.3 order_points() 函数 3.1.4 four_point_transform() 函数 3.2 读取图片并做预处理3.3 轮廓检测3.4 获取最大…

并发设计模式实战系列(3):工作队列

&#x1f31f; ​大家好&#xff0c;我是摘星&#xff01;​ &#x1f31f; 今天为大家带来的是并发设计模式实战系列&#xff0c;第三章工作队列&#xff08;Work Queue&#xff09;​​&#xff0c;废话不多说直接开始~ 目录 一、核心原理深度拆解 1. 生产者-消费者架构 …

如何理解抽象且不易理解的华为云 API?

API的概念在华为云的使用中非常抽象&#xff0c;且不容易理解&#xff0c;用通俗的语言 形象的比喻来讲清楚——什么是华为云 API&#xff0c;怎么用&#xff0c;背后原理&#xff0c;以及主要元素有哪些&#xff0c;尽量让新手也能明白。 &#x1f9e0; 一句话先理解&#xf…

深度学习-全连接神经网络(过拟合,欠拟合。批量标准化)

七、过拟合与欠拟合 在训练深层神经网络时&#xff0c;由于模型参数较多&#xff0c;在数据量不足时很容易过拟合。而正则化技术主要就是用于防止过拟合&#xff0c;提升模型的泛化能力(对新数据表现良好)和鲁棒性&#xff08;对异常数据表现良好&#xff09;。 1. 概念认知 …

系统架构设计师:流水线技术相关知识点、记忆卡片、多同类型练习题、答案与解析

流水线记忆要点‌ ‌公式 总时间 (n k - 1)Δt 吞吐率 TP n / 总时间 → 1/Δt&#xff08;max&#xff09; 加速比 S nk / (n k - 1) | 效率 E n / (n k - 1) 关键概念 周期&#xff1a;最长段Δt 冲突‌&#xff1a; ‌数据冲突&#xff08;RAW&#xff09; → 旁路/…

复刻低成本机械臂 SO-ARM100 3D 打印篇

视频讲解&#xff1a; 复刻低成本机械臂 SO-ARM100 3D 打印篇 清理了下许久不用的3D打印机&#xff0c;挤出机也裂了&#xff0c;更换了喷嘴和挤出机夹具&#xff0c;终于恢复了正常工作的状态&#xff0c;接下来还是要用起来&#xff0c;不然吃灰生锈了&#xff0c;于是乎想起…

Flutter IOS 真机 Widget 错误。Widget 安装后系统中没有

错误信息&#xff1a; SendProcessControlEvent:toPid: encountered an error: Error Domaincom.apple.dt.deviceprocesscontrolservice Code8 "Failed to show Widget com.xxx.xxx.ServerStatus error: Error DomainFBSOpenApplicationServiceErrorDomain Code1 "T…