WebRTC服务质量(09)- Pacer机制(01) 流程概述

WebRTC服务质量(01)- Qos概述
WebRTC服务质量(02)- RTP协议
WebRTC服务质量(03)- RTCP协议
WebRTC服务质量(04)- 重传机制(01) RTX NACK概述
WebRTC服务质量(05)- 重传机制(02) NACK判断丢包
WebRTC服务质量(06)- 重传机制(03) NACK找到真正的丢包
WebRTC服务质量(07)- 重传机制(04) 接收NACK消息
WebRTC服务质量(08)- 重传机制(05) RTX机制
WebRTC服务质量(09)- Pacer机制(01) 流程概述
WebRTC服务质量(10)- Pacer机制(02) RoundRobinPacketQueue
WebRTC服务质量(11)- Pacer机制(03) IntervalBudget
WebRTC服务质量(12)- Pacer机制(04) 向Pacer中插入数据

一、前言:

Pacer 是一种数据发送调度机制。它的主要功能是根据网络带宽限制、网络拥塞控制的反馈以及媒体的发送策略,对数据包的发送进行适配和节奏调度,以避免网络拥塞、减少丢包并保证流媒体传输的平滑性。

二、核心概念:

2.1 Pacer 的作用:

  • 流量平滑:音频、视频等媒体数据包可能在生成时间上分布不均,通过 Pacer,可以将这些数据包的发送节奏调整成更均匀的形式,避免突发流量冲击网络。
  • 速率控制:Pacer 会根据传输码率(由拥塞控制算法提供,例如 Google 的 Congestion Control),动态调整包的发送速率,确保发送端不超过网络容量。
  • 优先级处理:Pacer 支持区分不同的流类型(例如音频、视频、FEC 冗余流等),为高优先级流(如音频)留出更多资源或者保证其更低的延迟。

2.2 Pacer 的工作流程:

  1. 音视频流媒体按照编码器的设定发送数据包。
  2. 数据包会先进入一个队列(Packet Queue)。
  3. Pacer 通过定时器或事件驱动机制,根据当前允许的发送速率(由拥塞控制模块动态提供)从队列中提取数据包发送。
  4. 如果发送速率受限,多余的包会继续留在队列中等待下一轮调度。

以下是简化的机制模型:

[Media Encoder] -> [Packet Queue] -> Pacer -> [Network Sender]

三、核心代码:

先想想,根据前面的概念阐述,如果要让你写,你怎么写呢?是不是重点实现以下几个模块:

  • packet_router.h/cpp:负责将数据包分发到具体的网络发送端。
  • packet_sender.h/cpp:实现了调度核心逻辑。
  • packet_queue.h/cpp:实现用于存储待发送数据包的队列。

现在具体看看webrtc如何做的。

3.1、Pacer对象创建:

  • PeerConnection当中创建Call对象;

    // 从上到下调用顺序如下:
    PeerConnectionFactory::CreatePeerConnectionOrError -->
    PeerConnectionFactory::CreateCall_w -->
    CallFactory::CreateCall --> 
    Call::Create
    
  • Call对象创建RtpTransportControllerSend:

    创建Call时候首先得创建Call::Config,便于以后创建Pacer对象时候作为参数;

    Call* Call::Create(const Call::Config& config,
                       Clock* clock,
                       rtc::scoped_refptr<SharedModuleThread> call_thread,
                       std::unique_ptr<ProcessThread> pacer_thread) {
      RTC_DCHECK(config.task_queue_factory);
      return new internal::Call(
          clock, config,
          std::make_unique<RtpTransportControllerSend>(
              clock, config.event_log, config.network_state_predictor_factory,
              config.network_controller_factory, config.bitrate_config,
              std::move(pacer_thread), config.task_queue_factory, config.trials),
          std::move(call_thread), config.task_queue_factory);
    }
    

    这儿的config.trials,会导致后面创建PacedSender对象的时候,处理模式为kPeriodic,也就是周期处理模式;

  • RtpTransportControllerSend创建PacerSender对象:

    RtpTransportControllerSend 是 WebRTC 中负责发送侧 RTP 流量控制的核心模块。它是 RTP 传输的控制器,负责管理和协调发送侧的各种功能,包括比特率管理(带宽估算 BWE)、节奏控制(Pacer)、网络状态预测和反馈处理等。我们先看看构造函数:

    RtpTransportControllerSend::RtpTransportControllerSend(
        Clock* clock,
        webrtc::RtcEventLog* event_log,
        NetworkStatePredictorFactoryInterface* predictor_factory,
        NetworkControllerFactoryInterface* controller_factory,
        const BitrateConstraints& bitrate_config,
        std::unique_ptr<ProcessThread> process_thread,
        TaskQueueFactory* task_queue_factory,
        const WebRtcKeyValueConfig* trials)
        : clock_(clock),
          event_log_(event_log),
          bitrate_configurator_(bitrate_config),
          process_thread_started_(false),
          process_thread_(std::move(process_thread)),
          use_task_queue_pacer_(IsEnabled(trials, "WebRTC-TaskQueuePacer")),
          process_thread_pacer_(use_task_queue_pacer_
                                    ? nullptr
                                    : new PacedSender(clock,
                                                      &packet_router_,
                                                      event_log,
                                                      trials,
                                                      process_thread_.get())),
          task_queue_pacer_(
              use_task_queue_pacer_
                  ? new TaskQueuePacedSender(
                        clock,
                        &packet_router_,
                        event_log,
                        trials,
                        task_queue_factory,
                        /*hold_back_window = */ PacingController::kMinSleepTime)
                  : nullptr),
          observer_(nullptr),
          controller_factory_override_(controller_factory),
          controller_factory_fallback_(
              std::make_unique<GoogCcNetworkControllerFactory>(predictor_factory)),
          process_interval_(controller_factory_fallback_->GetProcessInterval()),
          last_report_block_time_(Timestamp::Millis(clock_->TimeInMilliseconds())),
          reset_feedback_on_route_change_(
              !IsEnabled(trials, "WebRTC-Bwe-NoFeedbackReset")),
          send_side_bwe_with_overhead_(
              !IsDisabled(trials, "WebRTC-SendSideBwe-WithOverhead")),
          add_pacing_to_cwin_(
              IsEnabled(trials, "WebRTC-AddPacingToCongestionWindowPushback")),
          relay_bandwidth_cap_("relay_cap", DataRate::PlusInfinity()),
          transport_overhead_bytes_per_packet_(0),
          network_available_(false),
          retransmission_rate_limiter_(clock, kRetransmitWindowSizeMs),
          task_queue_(task_queue_factory->CreateTaskQueue(
              "rtp_send_controller",
              TaskQueueFactory::Priority::NORMAL)) {
      ParseFieldTrial({&relay_bandwidth_cap_},
                      trials->Lookup("WebRTC-Bwe-NetworkRouteConstraints"));
      initial_config_.constraints = ConvertConstraints(bitrate_config, clock_);
      initial_config_.event_log = event_log;
      initial_config_.key_value_config = trials;
      RTC_DCHECK(bitrate_config.start_bitrate_bps > 0);
    
      pacer()->SetPacingRates(
          DataRate::BitsPerSec(bitrate_config.start_bitrate_bps), DataRate::Zero());
    
      if (absl::StartsWith(trials->Lookup("WebRTC-LazyPacerStart"), "Disabled")) {
        EnsureStarted();
      }
    }
    

    我们分段看下,主要分为四大步:

    (1) Pacer 类型选择

    构造函数根据配置决定使用 TaskQueuePacedSender 或传统的 PacedSender

    use_task_queue_pacer_(IsEnabled(trials, "WebRTC-TaskQueuePacer")),
    process_thread_pacer_(use_task_queue_pacer_
                          ? nullptr
                          : new PacedSender(clock, &packet_router_, event_log, trials, process_thread_.get())),
    task_queue_pacer_(use_task_queue_pacer_
                      ? new TaskQueuePacedSender(clock, &packet_router_, event_log, trials,
                                                 task_queue_factory,
                                                 /*hold_back_window=*/PacingController::kMinSleepTime)
                      : nullptr),
    
    • 如果 WebRTC-TaskQueuePacer 被启用,task_queue_pacer_ 将被初始化,表示使用 TaskQueue 驱动的实现。
    • 如果未启用,则使用传统的 PacedSender,并传入线程 process_thread_ 用于驱动调度。
    (2) 任务队列初始化

    任务队列 task_queue_ 的创建逻辑:

    task_queue_(task_queue_factory->CreateTaskQueue(
                  "rtp_send_controller",
                  TaskQueueFactory::Priority::NORMAL))
    
    • rtp_send_controller 创建了一个中等优先级的任务队列。这意味着发送侧 RTP 控制器的任务拥有一定优先,但不会抢占核心实时任务(如音频编码)。
    (3) 启动策略

    根据 WebRTC-LazyPacerStart 配置决定是否延迟启动 Pacer:

    if (absl::StartsWith(trials->Lookup("WebRTC-LazyPacerStart"), "Disabled")) {
      EnsureStarted();
    }
    
    • “Lazy Start” 功能通常用于优化资源启动时的开销。如果禁用延迟启动,则直接调用 EnsureStarted() 启动所有功能。
    (4) Pacer 初始化

    为 Pacer 设置初始的码率:

    pacer()->SetPacingRates(
        DataRate::BitsPerSec(bitrate_config.start_bitrate_bps), DataRate::Zero());
    
    • SetPacingRates 需要传入目标的比特率(start_bitrate_bps)和填充数据的比特率(这里为 0)。

3.2、PacedSender:

PacedSender 是 WebRTC 中最核心的 Pacer 调度类,负责实现基于速率的节奏化数据发送。

PacedSender::PacedSender(Clock* clock,
                         PacketRouter* packet_router,
                         RtcEventLog* event_log,
                         const WebRtcKeyValueConfig* field_trials,
                         ProcessThread* process_thread)
    : process_mode_( 
          (field_trials != nullptr &&
           absl::StartsWith(field_trials->Lookup("WebRTC-Pacer-DynamicProcess"), "Enabled"))
              ? PacingController::ProcessMode::kDynamic
              : PacingController::ProcessMode::kPeriodic),
      pacing_controller_(clock, // 构造 PacingController
                         packet_router,
                         event_log,
                         field_trials,
                         process_mode_),
      clock_(clock),
      process_thread_(process_thread) {
  if (process_thread_)
    process_thread_->RegisterModule(&module_proxy_, RTC_FROM_HERE);
}

PacedSender 主要干两件事:

  • 设置PacedSender 处理模式为周期模式ProcessMode::kPeriodic (field_trials带回来的);
  • 创建了一个PacingController对象到pacing_controller_ ,Pacer模块中的主要逻辑都是由PacingController完成的;

构建好PacedSenderPacingCOntroller对象之后,最后启动Pacer线程,以后所有需要发送的数据包,都需要通过PacingControllerProcessPackets去发送。

3.3、两个重要函数:

1)PacingController::NextSendTime():

Pacing模块当中,获取每次执行的时间(5ms)

Timestamp PacingController::NextSendTime() const {
  // 。。。
  // kPeriodic模式(其实目前也仅仅支持这种周期模式)
  if (mode_ == ProcessMode::kPeriodic) {
    // In periodic non-probing mode, we just have a fixed interval.
    // 在周期性非探测模式下,我们只有一个固定间隔。就是使用上次处理的时间,加上min_packet_limit_(默认是5ms)
    return last_process_time_ + min_packet_limit_;
  }
  // ...
  return last_process_time_ + kPausedProcessInterval;
}

2)PacingController::ProcessPackets():

周期发送数据包。这个函数同样很长,我只保留关键逻辑:

  • 关键逻辑:
void PacingController::ProcessPackets() {
  // 动态模式目前版本不会用到(现在只用到周期模式)
  if (mode_ == ProcessMode::kDynamic) {
      // 。。。
  }
  // 判断当前是否应该发送保活包(当没有发送音视频数据的是时候,应该发送保活)
  if (ShouldSendKeepalive(now)) {
     // 。。。
  }
  // 如果处于暂停状态(重新进行媒体协商的时候,需要置为暂停状态),不能发送音视频数据包
  if (paused_) {
    return;
  }
  // 如果逝去的时间大于0(说明已经过了一段时间了),为media budget设置目标码率
  // webrtc是通过media budget设置目标码率的,ProcessPackets 正是根据media budget控制码流大小的
  // 如果发现media budget是500kbps,那么每次处理就是按照500kbps进行发送
  if (elapsed_time > TimeDelta::Zero()) {
     // 将前面设置给pacer的目标码率设置给media_budget
  }

  // 是否需要探测带宽码率,这块主要根据拥塞控制来
  if (is_probing) {
      // 获得pacing_info
  }

  // The paused state is checked in the loop since it leaves the critical
  // section allowing the paused state to be changed from other code.
  // 核心逻辑
  while (!paused_) { // 是否处于暂停状态
      // 下面单独分析
  }
  if (is_probing) {
     // 发送探测包
  }
}
  • 核心逻辑:(同样因为代码很多,只保留关键逻辑)
  // 核心逻辑
  while (!paused_) { // 是否处于暂停状态
    // Fetch the next packet, so long as queue is not empty or budget is not
    // exhausted.
    // 从Packet队列( RoundRobinPacketQueue packet_queue_ )中,取出我们要发送的packet
    // 如果我们获取到有数据的packet,那么就发送出去,但是如果我们没有获取到有数据的packet,rtp_packet为null,又分为两种情况:
    // 1.packet队列的确没有音视频数据包,这个时候,我们应该发送padding,让底层网络给我们将带宽留着
    //   否则,底层网络会将带宽进行回收,等到真正需要发送数据的时候,没有带宽,会造成延迟;
    // 2.有数据,但是我们现在已经发送的数据,达到了我们设定的目标码流,也不能让发送了。
    std::unique_ptr<RtpPacketToSend> rtp_packet =
        GetPendingPacket(pacing_info, target_send_time, now);
    if (rtp_packet == nullptr) {
      // 没有获取到数据包
      // 1、检查是否应该发送padding包,如果需要,将Padding包插入队列,并重新执行while循环。
      // 2、如果不需要,则退出while循环,说明这次不能再发送数据了。
      DataSize padding_to_add = PaddingToAdd(recommended_probe_size, data_sent);
      if (padding_to_add > DataSize::Zero()) {
        std::vector<std::unique_ptr<RtpPacketToSend>> padding_packets =
            packet_sender_->GeneratePadding(padding_to_add);
        if (padding_packets.empty()) {
          // No padding packets were generated, quite send loop.
          break;
        }
        for (auto& packet : padding_packets) {
          EnqueuePacket(std::move(packet));
        }
        // Continue loop to send the padding that was just added.
        continue;
      }

      // Can't fetch new packet and no padding to send, exit send loop.
      break;
    }

    // 否则,说明我们获取了一个packet,那么计算packet的payload 大小是多少字节
    const RtpPacketMediaType packet_type = *rtp_packet->packet_type();
    DataSize packet_size = DataSize::Bytes(rtp_packet->payload_size() +
                                           rtp_packet->padding_size());
    // 将Packet发送出去
    packet_sender_->SendPacket(std::move(rtp_packet), pacing_info);
    for (auto& packet : packet_sender_->FetchFec()) {
      EnqueuePacket(std::move(packet));
    }
    data_sent += packet_size;

    // Send done, update send/process time to the target send time.
    // 记录发送的packet大小和时间(下次的发送时间就是根据我们这次更新的时间计算出来的)
    OnPacketSent(packet_type, packet_size, target_send_time);
  }

核心逻辑就是发数据包,有媒体数据就考虑发送媒体数据,没有就发送填充数据,进行保活,当然,发送两者的时候都应该考虑目标码率,已经超过我们设置的目标码率就等等再发。

  • 获取待发送的数据包:

    看看上面获取数据包的函数GetPendingPacket究竟干了啥:

    std::unique_ptr<RtpPacketToSend> PacingController::GetPendingPacket(
        const PacedPacketInfo& pacing_info,
        Timestamp target_send_time,
        Timestamp now) {
      if (packet_queue_.Empty()) {
        return nullptr;
      }
    
      // First, check if there is any reason _not_ to send the next queued packet.
    
      // Unpaced audio packets and probes are exempted from send checks.
      // 不需要平滑处理的特殊情况:
      // pace_audio_ 为false,说明音频包不需要平滑处理,并且,
      // packet_queue_.LeadingAudioPacketEnqueueTime().has_value() 说明队列中下一个数据包是音频包
      bool unpaced_audio_packet =
          !pace_audio_ && packet_queue_.LeadingAudioPacketEnqueueTime().has_value();
      bool is_probe = pacing_info.probe_cluster_id != PacedPacketInfo::kNotAProbe;
      if (!unpaced_audio_packet && !is_probe) {
        // 网络拥塞状况下不能发送
        if (Congested()) {
          // Don't send anything if congested.
          return nullptr;
        }
        // 没有拥塞,会检测模式是否为周期模式
        if (mode_ == ProcessMode::kPeriodic) {
          // media_budget_ 中可供我们使用的字节数不大于0,表示我们已经达到了目标码率,我们也不发送
          if (media_budget_.bytes_remaining() <= 0) {
            // Not enough budget.
            return nullptr;
          }
        } else {
          // 动态模式现在还不成熟,不关心
        }
      }
      // 如果不是上面的情况,那么,我们需要从队列中pop出一个packet,让ProcessPackets函数将其发送出去
      return packet_queue_.Pop();
    }
    
    

    主要就是先做了一些发送状态的校验:网络拥塞情况下不能发送、达到目标码率了也不能发送,如果校验通过可以发送,就从队列中取出一个数据包进行发送。

四、总结:

本文主要介绍了Pacer模块的作用,以及相关重要的框架代码,其实还有很多细节后面再打开看看。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

BAPI_BATCH_CHANGE在更新后不自动更新批次特征

1、问题介绍 在CL03中看到分类特性配置了制造日期字段&#xff0c;并绑定了生产日期字段MCH1~HSDAT MSC2N修改批次的生产日期字段时&#xff0c;自动修改了对应的批次特性 但是通过BAPI&#xff1a;BAPI_BATCH_CHANGE修改生产日期时&#xff0c;并没有更新到批次特性中 2、BAPI…

SQL中的窗口函数

1.窗口函数简介 窗口函数是SQL中的一项高级特性&#xff0c;用于在不改变查询结果集行数的情况下&#xff0c;对每一行执行聚合计算或者其他复杂的计算&#xff0c;也就是说窗口函数可以跨行计算&#xff0c;可以扫描所有的行&#xff0c;并把结果填到每一行中。这些函数通常与…

转运机器人推动制造业智能化转型升级

​在当今制造业智能化转型的浪潮中&#xff0c;技术创新成为企业脱颖而出的关键。富唯转运机器人凭借一系列先进技术&#xff0c;成为智能转型的卓越之选。 一体化 AMR 控制系统是富唯的一大亮点。它采用低代码流程搭建和配置模式&#xff0c;极大地缩短了部署时间。企业无需耗…

同步异步日志系统:设计模式

设计模式是前辈们对代码开发经验的总结&#xff0c;是解决特定问题的⼀系列套路。它不是语法规定&#xff0c;⽽是⼀ 套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。 为什么会产生设计模式这样的东西呢&#xff1f;就像人类历史发展会产生兵法。最开…

数据分析思维(五):分析方法——假设检验分析方法

数据分析并非只是简单的数据分析工具三板斧——Excel、SQL、Python&#xff0c;更重要的是数据分析思维。没有数据分析思维和业务知识&#xff0c;就算拿到一堆数据&#xff0c;也不知道如何下手。 推荐书本《数据分析思维——分析方法和业务知识》&#xff0c;本文内容就是提取…

AppInventor2 ClientSocketAI2Ext 拓展加强版 - 为App提供TCP客户端接入,可发送二进制数据

本文介绍App Inventor 2利用拓展实现TCP/IP协议接入功能&#xff0c;作为网络客户端连接TCP服务器&#xff0c;进行数据通信&#xff08;发送/接收&#xff09;。 // ClientSocketAI2Ext 拓展现状 // 原版拓展名称为&#xff1a;com.gmail.at.moicjarod.aix&#xff0c;是能用…

Docker-构建自己的Web-Linux系统-镜像webtop:ubuntu-kde

介绍 安装自己的linux-server,可以作为学习使用&#xff0c;web方式访问&#xff0c;基于ubuntu构建开源项目 https://github.com/linuxserver/docker-webtop安装 docker run -d -p 1336:3000 -e PASSWORD123456 --name webtop lscr.io/linuxserver/webtop:ubuntu-kde登录 …

随身 WiFi 连接 X-Wrt 共享网络与 IPv6 中继配置

本文首发于只抄博客&#xff0c;欢迎点击原文链接了解更多内容。 前言 之前分享的《随身 WiFi 通过 USB 连接路由器共享网络 扩展网络覆盖范围》介绍了随身 WiFi 通过 USB 连接到路由器共享网络&#xff0c;其中留下两个小问题没有解决&#xff1a; OpenWrt 无法识别中兴微的…

3.银河麒麟V10 离线安装Nginx

1. 下载nginx离线安装包 前往官网下载离线压缩包 2. 下载3个依赖 openssl依赖&#xff0c;前往 官网下载 pcre2依赖下载&#xff0c;前往Git下载 zlib依赖下载&#xff0c;前往Git下载 下载完成后完整的包如下&#xff1a; 如果网速下载不到请使用网盘下载 通过网盘分享的文件…

家用无线路由器的 2.4GHz 和 5GHz

家中的无线路由器 WiFi 名称有两个&#xff0c;一个后面带有 “5G” 的标记&#xff0c;这让人产生疑问&#xff1a;“连接带‘5G’的 WiFi 是不是速度更快&#xff1f;” 实际上&#xff0c;这里的 “5G” 并不是移动通信中的 5G 网络&#xff0c;而是指路由器的工作频率为 5G…

【HarmonyOS NEXT】鸿蒙原生应用“上述”

鸿蒙原生应用“上述”已上架华为应用市场&#xff0c;欢迎升级了鸿蒙NEXT系统的用户下载体验&#xff0c;用原生更流畅。 个人CSDN鸿蒙专栏欢迎订阅&#xff1a;https://blog.csdn.net/weixin_44640245/category_12536933.html?fromshareblogcolumn&sharetypeblogcolumn&a…

AI开发:使用支持向量机(SVM)进行文本情感分析训练 - Python

支持向量机是AI开发中最常见的一种算法。之前我们已经一起初步了解了它的概念和应用&#xff0c;今天我们用它来进行一次文本情感分析训练。 一、概念温习 支持向量机&#xff08;SVM&#xff09;是一种监督学习算法&#xff0c;广泛用于分类和回归问题。 它的核心思想是通过…

Linux部署spring项目基础教程

目录 一、安装jdk(yum安装) 1.查看是否有jdk ​编辑 2.查找你想安装的jdk版本 3.安装你需要的版本 4.重复第一步查看版本号,看到版本号说明安装成本 二、部署服务 1.上传jar包 2.启动服务 3.脚本启动 自己搞了个服务器,部署了一个demo项目,把部署流程记录下 一、…

JS中的原型与原型链

1. 基本概念 原型&#xff08;Prototype&#xff09;&#xff1a;每个对象都有一个内部属性 [[Prototype]]&#xff0c;通常通过 __proto__ 访问&#xff08;非标准&#xff0c;但广泛支持&#xff09;。 原型链&#xff08;Prototype Chain&#xff09;&#xff1a;对象通过原…

如何从 0 到 1 ,打造全新一代分布式数据架构

导读&#xff1a;本文从 DIKW&#xff08;数据、信息、知识、智慧&#xff09; 模型视角出发&#xff0c;探讨数字世界中数据的重要性问题。接着站在业务视角&#xff0c;讨论了在不断满足业务诉求&#xff08;特别是 AI 需求&#xff09;的过程中&#xff0c;数据系统是如何一…

Docker完整技术汇总

Docker 背景引入 在实际开发过程中有三个环境&#xff0c;分别是&#xff1a;开发环境、测试环境以及生产环境&#xff0c;假设开发环境中开发人员用的是jdk8&#xff0c;而在测试环境中测试人员用的时jdk7&#xff0c;这就导致程序员开发完系统后将其打成jar包发给测试人员后…

华为 AI Agent:企业内部管理的智能变革引擎(11/30)

一、华为 AI Agent 引领企业管理新潮流 在当今数字化飞速发展的时代&#xff0c;企业内部管理的高效性与智能化成为了决定企业竞争力的关键因素。华为&#xff0c;作为全球领先的科技巨头&#xff0c;其 AI Agent 技术在企业内部管理中的应用正掀起一场全新的变革浪潮。 AI Ag…

Idea使用阿里云创建springboot项目

文章目录 创建springboot项目选择Spring Initializr配置Server URL 创建springboot项目 选择Spring Initializr 配置Server URL https://start.aliyun.com

安全教育培训小程序系统开发制作方案

安全教育培训小程序系统是为了提高公众的安全意识&#xff0c;普及安全知识&#xff0c;通过微信小程序的方式提供安全教育培训服务&#xff0c;帮助用户了解并掌握必要的安全防范措施。 一、目标用户 企业员工&#xff1a;各岗位员工&#xff0c;特别是IT部门、财务、行政等对…

MySQL 数据”丢失”事件之 binlog 解析应用

事件背景 客户反馈在晚间数据跑批后,查询相关表的数据时,发现该表的部分数据在数据库中不存在 从应用跑批的日志来看,跑批未报错,且可查到日志中明确显示当时那批数据已插入到数据库中 需要帮忙分析这批数据丢失的原因。 备注:考虑信息敏感性,以下分析场景测试环境模拟,相关数据…