Transactional outbox pattern

news2024/11/15 19:38:26

文章目录

  • Transactional outbox pattern
    • 事件驱动架构(Event Driven Architecture, EDA)
    • 数据库事务和消息发布的一致性问题
    • Transactional outbox如何解决数据事务和消息发布之间的一致性问题
    • 如何实现Transactional outbox pattern
    • 消息幂等性问题
    • Transactional outbox pattern能保证最终一致性吗?
      • 消息的可靠性
        • Producer的可靠性
        • Broker的可靠性
        • Consumer的可靠性
    • Transactional outbox pattern能保证消息顺序性吗?
      • 队列投递顺序 (Broker Delivery Order)
      • 业务事件顺序 (Business Event Order)
    • 我们真的需要引入Transactional outbox pattern吗?
    • 参考和学习资料

Transactional outbox pattern

事件驱动架构(Event Driven Architecture, EDA)

许多现代应用都采用了事件驱动设计。因为事件驱动架构可以最大程度减少耦合度,因此是现代化分布式应用架构的理想之选。

与传统的请求/响应驱动模型(同步)不同的是,事件驱动架构耦合性更低,因为事件发起者并不知道哪个事件使用者在监听事件,而且事件也不知道其所产生的后续结果。

在这里插入图片描述

上图来源于:What is microservices architecture?

一个好的微服务应该是一个松耦合的服务,应该尽可能少地知道与之协作的那些服务的信息,所以在微服务开发中,我们常常会使用事件驱动架构来帮我们打造一个更好的松耦合的服务。

数据库事务和消息发布的一致性问题

当我们需要发送消息并提交一些数据库更改时,存在一个难题,我们应该在事务中还是在事务之后发送消息?

通常的业务处理过程中我们都会更新数据库(包括create,update,delete)然后发送message/event 给 message broker(消息中间件)。我们需要保证数据库更新和事件发布之间的原子性,也即要么二者都成功,要么都失败

在这里插入图片描述

下图在事务中发送消息。 这会有问题,如果步骤5事务commit失败,发生回滚;但此时消息已经发送出去,就有不一致的情况。

在这里插入图片描述

在这里插入图片描述

下图在事务commit之后发送消息。 这也存在问题,如果步骤4的发送失败,则下游系统将永远不会收到该消息。

在这里插入图片描述

在传统的实践方式中,Global Transaction(全局事务)和 XA Transaction(分布式事务)通常用于解决此类问题,它们都是利用 2PC(两阶段提交)协议来保证事务一致性,通过协调者来确保多个参与者之间的事务保持一致性。然而,这种事务本身的效率是很低的,另外,一些技术框架并不提供对全局事务的支持。

Transactional outbox如何解决数据事务和消息发布之间的一致性问题

为了解决这个问题,ebay 架构师 Dan Pritchett 在 2008 年发表的一篇论文: Base: An Acid Alternative中描述了 outbox模式。 在这种模式下,我们有一个outbox表来保存要发送的消息。 该消息与数据库事务一起提交,这确保了该消息与业务数据原子地持久化。 然后异步作业将扫描待处理的消息,并将其发布到Broker,如果成功,则将消息更新为“已发送”,并在以后进行归档。

我们来看看Transactional outbox是如何解决数据库更新和事件发布之间的原子性问题。

下图来源于:Pattern: Transactional outbox

在这里插入图片描述

大致的流程如下:

  1. Order Service接受用户请求;
  2. 在同一个事务下处理用户请求;
  3. 写入ORDER table业务表;
  4. 写入OUTBOX table事件表,ORDER table和OUTBOX table更新在同一个本地数据库事务中;
  5. 事务完成后,需要去触发事件的发送(比如可以通过Spring AOP的方式完成,也可以定时扫描事件表,还可以借助诸如MySQL的binlog之类的机制比如Change data capture (CDC)的方式);
  6. 后台任务读取OUTBOX table事件表;
  7. 后台任务发送事件到消息队列;
  8. 发送成功后删除事件。

在这里插入图片描述

在这里插入图片描述

通过在数据库中创建 Outbox 表,将要发送的消息记录在 Outbox 表中,然后在事务提交之后,异步地读取 Outbox 表中的消息并将其发布到消息队列中。并且通过定时任务的方式去拉取未成功发送的消息去发送,来确保最终事务和消息发布的原子性。

如何实现Transactional outbox pattern

下面是我对于Transactional outbox pattern提供的一些思路和伪代码:

在这里插入图片描述

首先我们需要定义出outbox table,outbox table中我们需要定义几个重要的字段:channel(retry的时候可以知道要往哪个channel发送),mssageStatus(标记当前message是发送成功还是失败),messagePayload(保存当前发送的消息实体,以便retry的时候进行发送),retryTimes(记录retry次数,用于控制最大的retry次数)。其他的字段可以根据自己的业务要求进行定制。

@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "outbox_table")
public class OutboxEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String channel;
  private String messageBody;
  private String messageStatus;
  private Integer retryTimes;
  //more...
}

这里我是用spring-cloud-stream-3.2.4版本为例,写一些简单的伪代码

@Slf4j
@RequiredArgsConstructor
@Component
public class MessageSender {
  private final StreamBridge streamBridge;
  private final OutboxRepository outboxRepository;
  private final ApplicationContext applicationContext;

  public <T extends BaseEvent> void sendMessage(String bindingChannel, T event) {
    //Make sure it's in the same transaction
    OutboxEntity outbox = outboxRepository.save(OutboxEntity.builder()
            .channel(bindingChannel)
            .messageStatus("Ready To Send")
            .messageBody(JsonUtils.toJson(event))
            .retryTimes(0).build());
    applicationContext.publishEvent(outbox);
  }


  @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
  public void sendMessageAfterCommit(OutboxEntity outbox) {
    boolean isSendSuccess = streamBridge.send(outbox.getChannel(), MessageBuilder.withPayload(outbox.getMessageBody()).build());
    if (isSendSuccess) {
      outbox.setMessageStatus("Sent");
      outboxRepository.save(outbox);
    }
  }
}

streamBridge.send方法里面的实现是同步的,在方法里面最后会调用messageChannel.send方法

public boolean send(String bindingName, @Nullable String binderName, Object data, MimeType outputContentType) {
		ProducerProperties producerProperties = this.bindingServiceProperties.getProducerProperties(bindingName);
		MessageChannel messageChannel = this.resolveDestination(bindingName, producerProperties, binderName);

		Function functionToInvoke = this.getStreamBridgeFunction(outputContentType.toString(), producerProperties);

		if (producerProperties != null && producerProperties.isPartitioned()) {
			functionToInvoke = new PartitionAwareFunctionWrapper(functionToInvoke, this.applicationContext, producerProperties);
		}

		String targetType = this.resolveBinderTargetType(bindingName, binderName, MessageChannel.class,
			this.applicationContext.getBean(BinderFactory.class));

		Message<?> messageToSend = data instanceof Message
				? MessageBuilder.fromMessage((Message) data).setHeaderIfAbsent(MessageUtils.TARGET_PROTOCOL, targetType).build()
						: new GenericMessage<>(data, Collections.singletonMap(MessageUtils.TARGET_PROTOCOL, targetType));

		Message<?> resultMessage;
		synchronized (this) {
			resultMessage = (Message<byte[]>) functionToInvoke.apply(messageToSend);
		}

		resultMessage = (Message<?>) this.functionInvocationHelper.postProcessResult(resultMessage, null);

		return messageChannel.send(resultMessage);

	}

我们从MessageChannel的send方法注释可以看出来,返回值代表该消息是否已经发送出去了。所以我们利用该返回值来判断是否需要修改messageStatus的状态。

/**
	 * Send a {@link Message} to this channel. If the message is sent successfully,
	 * the method returns {@code true}. If the message cannot be sent due to a
	 * non-fatal reason, the method returns {@code false}. The method may also
	 * throw a RuntimeException in case of non-recoverable errors.
	 * <p>This method may block indefinitely, depending on the implementation.
	 * To provide a maximum wait time, use {@link #send(Message, long)}.
	 * @param message the message to send
	 * @return whether the message was sent
	 */
	default boolean send(Message<?> message) {
		return send(message, INDEFINITE_TIMEOUT);
	}

接下来就是需要设置job定时去触发未发送成功的记录,为了防止服务的多个实例同时运行job而产生竞争或破坏消息的排序,我们这里使用另一个独立的微服务整合quartz实现分布式调度,然后去触发其他微服务去扫描outbox表并进行消息的发布。而且还有一点很重要,我们要注意poison message的处理,如果一个消息在扫出来之后多次处理都失败,那么你需要把它报告给support,不要让它阻碍其他正常消息的处理。所以我们这里会加入retryTimes字段,在每次触发之后都会累加,当达到最大的重试次数之后,这条数据就不会再被扫出来了。最后我们就可以把这些已经发送成功的outbox记录进行归档了。

还有一点很重要,就是每次从outbox table中拉取未发送成功的消息,需要limit一下条数,防止一次性拉取出很多的消息。

消息幂等性问题

上面提到的Message Relay可能不止一次地发布消息。例如,它可能在发布消息之后,但在还没有删除掉outbox表中对应的record就发生了服务宕机。当服务重新启动时,它将再次发布消息。所以消息是“至少一次投递”的。因此,消息的消费方必须是幂等的,即多次消费事件与单次消费该事件的效果相同。

在分布式系统中,消息可靠性是数据一致性的基础,通常需要一次或至少一次传递消息。实际上,严格来讲,没有人能够以完全一次(exactly once)的保证来实现消息传递系统,这是不可能实现的。实际上,我们让消费方至少一次处理幂等和排除重复数据,以模拟exactly once语义。所以作为消费方进行幂等性处理是很重要的。

Transactional outbox pattern能保证最终一致性吗?

很多人包括我之前也会认为使用了Transactional outbox pattern就能保证最终一致性了,其实这是错的。最终一致性需要很多条件的支持,其中最重要的两个条件就是

  1. 消息传递:为了实现数据的同步,我们需要可靠的消息传递系统,以确保消息的传递正确性、可靠性和有序性。
  2. 事务性支持:需要实现分布式事务支持,以保证多个节点上的数据修改操作具有原子性和一致性。

而Transactional outbox pattern只是保证了第二点,他保证了数据事务和消息发布之间的一致性。使用了Transactional outbox pattern之后消息总是在事务提交之后发送,这确保了事务操作和相应事件的消息发布是原子性的。因此,在可靠的消息传递系统中只要消息成功发送到消息队列中,最终一定会被处理,从而达到最终一致性的目的。

然而我们往往却忽略另一个重要的条件,那就是一个可靠的消息传递系统。

消息的可靠性

一个可靠的消息传递系统要求消息将始终及时传递。因此,最重要的是最终至少要传递一次消息而不会丢失。也就是需要保证Producer ,Broker和Consumer三个参与者都是可靠的。

Producer的可靠性

大多数MQ框架在生产者端都具有in-memory buffer或TCP Socket buffer,以提高批处理或管道性能。但是缓冲区是易失的,如果连接断开或进程崩溃,缓冲区中的消息可能会丢失。由于缓冲区的非持久性,仅当接收到ACK时,消息才被视为已发送。所以RabbitMQ和Kafka这些Broker都提供了ACK应答机制。

Broker的可靠性

即使我们在Broker和Producer之间具有ACK,在Broker端仍然有可能丢失消息。 这取决于Broker如何答复ACK。 如果消息尚未持久保存到磁盘时,Broker就回复ACK,则此时节点崩溃将导致Broker缓冲区中的消息丢失。

Consumer的可靠性

在消费者方面,通常我们需要原子处理数据库事务的消息。 换句话说,如果消息ACK失败,则不要提交事务,如果消息是ACK,则必须提交事务。

如果我们在提交事务之前对消息进行了确认,万一后面的数据库上的业务操作失败,这个消息相当于没有触发业务操作。这显然是不可接受的。

在这里插入图片描述

如果我们在事务中对消息进行了确认,我们会遇到和事务前确实一样的问题。比如我们在下图的第5步中未能提交事务,则由于已对消息进行了ACK,因此我们将再也没有机会再次使用它,数据库中的数据将永远不会 被更新。

在这里插入图片描述

处理此问题的更好方法是在事务处理后对消息进行确认。 如果事务已提交但消息未确认,我们稍后将再次收到这个消息。 如果您的消息处理是幂等的,你相当于具备了exactly-once的能力。见下图:

在这里插入图片描述

在 Spring Cloud Stream 中,我们一般使用默认的自动ACK模式就可以了。自动ACK模式:消费端消费消息之后,Spring Cloud Stream 会自动帮助发送ACK确认消息,此时机制应该是只要消费端正确消费消息且未发生异常,都会返回ACK确认消息。只有消费者正确消费并发送 ACK 确认消息时,才会将消息从队列中删除。如果消费者在处理消息期间发生异常或者 ACK 确认未正确发送,消息将会被重新投递。这意味着,即使消费者出现异常情况,消息也不会被立即丢弃而是重新放回队列中或者死信队列中等待下一次被消费。这种机制可以有效保证消息的可靠传递性,但也需要注意处理异常情况以免出现重复消费等问题。

Transactional outbox pattern能保证消息顺序性吗?

上面讲到了可靠的消息传递系统需要确保消息的传递正确性、可靠性和有序性,那Transactional outbox pattern能保证消息顺序性吗?答案很显然是不能完全保证消息的顺序性

正常情况下(不发生消息发送到Broker失败的情况),只要我们是按照顺序往outbox table存入数据,并且Message Relay使用单线程从outbox table顺序拉取消息,并把消息投递到Message Broker,那么在正常情况下(不发生消息发送到Broker失败的情况)我们是能保证消息是顺序发送到Broker的。

那如果消息是顺序发送到Broker的,那就能和下面图中的一样,保证消息消费的顺序性了吗? 这可不一定!

在这里插入图片描述

许多人谈论消息排序,但通常不同的人对消息顺序有不同的理解, 大家一起讨论消息顺序的时候经常是鸡同鸭讲。 在这里,我们明确定义了消息顺序的两个不同概念来避免混淆:队列投递顺序(Broker Delivery Order)和业务事件顺序(Business Event Order)。

队列投递顺序 (Broker Delivery Order)

队列投递顺序意味着,如果消息A在B之前到达Broker,则A也应该在B之前被消费。在Kafka/RabbitMQ中,单个分区(partition)中的消息按投递顺序消费的,满足Broker Delivery Order。

业务事件顺序 (Business Event Order)

业务事件顺序表示消息是按因果顺序生成和处理的。 例如,您的存款帐户从$0变为$1000,然后从$1000变为$500,下游消息消费者必须能够识别这两个事件的业务顺序。 如果下游首先收到事件$1000至$500,然后收到$0至$1000,则它必须能够识别出两条消息是乱序的,并在处理它们之前对其重新排序。

另一方面,如果将两个事务应用于两个帐户,并且这两个事务不在两个帐户之间操作,则可以说两个事务是独立的,之间没有因果关系,那么消费者可以按任何顺序处理这两个事件。 例如,事件A表示在帐户X上提款,事件B表示在帐户Y上存款,这两个事件无关紧要,没有因果关系,消费者可以按任何顺序对其进行处理。但是如果事务是从X转账到Y,再从Y转账到X,那么我们就要严格按照业务逻辑顺序重新排序和消费这两个消息。

因此,业务事件顺序是指辨别出因果相关的消息,无论broker投递顺序如何,我们都能按预期的业务顺序对其进行重新排序,丢弃和处理。 业务事件顺序才是您真正需要的“顺序”。

比如在使用Transactional outbox pattern并且保证了队列投递顺序 (Broker Delivery Order)的情况下,下面两个例子依然无法保证消息顺序消费(或者说是业务事件顺序)。

在这里插入图片描述

在这里插入图片描述

总而言之,我们根本不必太过关心消息是否顺序发送到broker或者是队列投递顺序,Transactional outbox pattern是能从一定程度上保证顺序,但我们不能过分依赖这个顺序,我们真正需要的是业务事件顺序。

我们真的需要引入Transactional outbox pattern吗?

从上面的流程看,引入Transactional outbox pattern之后系统复杂性增加了。所以我们有时候是需要思考一下在我们的系统中真的需要引入入Transactional outbox pattern吗?是否非要不可?有没有其他办法?我给出我个人的结论:我个人认为在中小型的微服务系统中都不是特别必要引入入Transactional outbox pattern。我的理由和解决方法有以下几点:

  1. 绝大数情况下都是happy case,而异常情况往往很少,那对于这些异常的情况我们能否通过监控的方式以及一些support tool来帮助我们进行消息的重发?
  2. 为了监控这些异常数据,我们可以引入一些业务流程中的中间状态,比如“处理中的订单“来帮助我们这些有“问题“的订单
  3. 之后我们可以通过support tool工具重新触发这些有“问题”的订单
  4. 或者我们可以从业务角度出发,给用户在界面提供一些reprocess之类的功能,让用户有办法让业务流程继续走下去

参考和学习资料

消息可靠性和顺序(中文)

What do you mean by “Event-Driven”?

Pattern: Transactional outbox

What is microservices architecture?

MQ 消息的可靠性投递

如何解决微服务的数据一致性分发问题?

spring cloud实现可靠消息一致性

分布式事务解决方案实战

研读《可靠消息最终一致性方案(本地消息服务)》

研读《可靠消息最终一致性方案(独立消息服务)》

【进阶之路】可靠消息最终一致性解决方案

可靠消息最终一致性分布式事务实现方案

可靠消息的最终一致性解决方案

Spring Cloud 实现可靠消息一致性

分布式消息队列:如何保证消息的可靠性传输

后端开发实践系列——事件驱动架构(EDA)编码实践

分布式事务?No, 最终一致性

(三)从0开始写框架—可靠消息事务最终一致性

分布式一致性协议之2PC和3PC

2PC和3PC

基于可靠消息服务的分布式事务

分布式事务——消息最终一致性方案

RocketMQ使用及分布式事务解决思路

基于MQ消息中间件的分布式事务解决方案

(微服务)分布式事务-最大努力交付 && 消息最终一致性方案

9.交易性能优化-事务型消息

Spring Cloud 分布式事务管理

可靠消息最终一致(异步确保型)

我说分布式事务之可靠消息最终一致性事务1-原理及实现

我说分布式事务之最大努力通知型事务

Implementing At Least Once Delivery With RabbitMQ and Spring’s RabbitTemplate

波波老师: 解决微服务的数据一致性分发问题?

空谈发件箱模式(outbox pattern)

聚焦JAVA性能优化 打造亿级流量秒杀系统【学习笔记】07_交易性能优化技术之事务型消息

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

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

相关文章

百万连接实现01:使用epoll+多线程+多IP地址管理tcp客户端集群

操作系统采用 <客户端IP : 客户端端口> : <服务端IP : 服务端端口> 四元组来标识一条TCP连接。 所以要想实现百万连接&#xff1a; 第一种是服务器端只开启一个进程&#xff0c;然后使用很多个客户端进程绑定不同的客户端 ip 来连接&#xff0c;假设 20个ip * 5w&a…

linux使用grep命令查询nginx的进程情况时总是出现 grep --color=auto nginx

问题&#xff1a; 每次使用ps aux | grep 服务名 命令查询某个服务的进程时&#xff0c;总会出现一条grep --colorauto 服务名 例如&#xff1a; ps aux | grep nginx # 会出现图片中的情况解答&#xff1a; 这是因为grep 也是一条命令&#xff0c;它在输出时&#xff0c;会…

《谈事说理》之《“脱节”的顶层设计》续集

欢迎收看本期《谈事说理》之《“脱节”的顶层设计》续集。当事人再次来到节目现场&#xff0c;为我们讲述近期事件的新发展。他与父母苦心经营的企业是否出现转机&#xff1f;我们一起来听听他的故事。 回忆事件经过&#xff0c;他的企业遭遇灭顶 当事人季博文&#xff08;化名…

Express-基础语法

介绍 Express是基于Node.js开发的第三方模块包&#xff0c;使用 Express&#xff0c;我们可以方便、快速的创建 Web 网站的服务器或 API 接口的服务器 参考 https://www.expressjs.com.cn/ 基本用法 安装 npm install express4.17.1 后面追加版本号&#xff0c;不写安装最新…

Docker 安装 MySQL 并使用 Navicat 连接

本文目录 1. 拉取 MySQL 镜像2. 创建并运行一个 MySQL 容器3. 验证MySQL容器是否创建并运行成功3.1 进入 MySQL 容器3.2 进入 MySQL3.3 查看 host 和 user 4. MySQL 开启远程访问权限4.1 切换数据库4.2 给 root 用户分配远程访问权限4.3 强制刷新权限 5. 服务器配置 3306 的开放…

iOS开发中的APP内活动之通用链接

首先&#xff0c;我们先来了解下APP内活动&#xff0c;这是苹果官方给的说明&#xff1a;https://developer.apple.com/cn/help/app-store-connect/offer-in-app-events/overview-of-in-app-events简单来说&#xff0c;就是我们在苹果后台开发者后台里填写关于自己APP内的一些具…

《MySQL》对表进行操作(DDL语句)

文章目录 &#x1f4a1;创建表&#x1f4a1;修改表&#x1f4a1;删除表 在了解下列语句前&#xff0c;先掌握一下指令 # 查看表内容 desc [表名] # 详细查看表内容 show create table [表名] \G&#x1f4a1;创建表 # 创建表 create table [表名] ([字段1] [类型1],[字段2] […

嵌入式 - UART使用进阶

UART – Advanced Features 概要 / Overview 最简单直接的使用UART接口的方式&#xff0c;是在轮循操作中来设置和处理UART接口。 轮询式UART的问题是轮询方式本身就是低效率的。 如果我们的UART被配置为115200的波特率和8N1&#xff0c;那么传输一个字符需要多长时间&#xff…

一款支持AI思维导图的AI助手——ChatMindAI

写在前面 Hello大家好&#xff0c; 我是【麟-小白】&#xff0c;一位软件工程专业的学生&#xff0c;喜好计算机知识。希望大家能够一起学习进步呀&#xff01;本人是一名在读大学生&#xff0c;专业水平有限&#xff0c;如发现错误或不足之处&#xff0c;请多多指正&#xff0…

“智慧水利”怎么建?这份智慧水利整体解决方案 值得参?

2021年&#xff0c;补齐水利信息化突出短板&#xff0c;提升强监管支撑能力。 2025 年&#xff0c;全面提升水利数字化、网络化水平&#xff0c;明显提升重点领域智能化水平。 2035 年&#xff0c;全面支撑水治理体系和治理能力现代化。 构建天空地一体化水利感知网&#xf…

python怎么使用Pillow库来添加图片水印

当使用Pillow库来添加图片水印时&#xff0c;下面是一个示例代码&#xff1a; 请确保在运行示例代码之前&#xff0c;已经安装了Pillow库&#xff08;可以使用pip install pillow命令进行安装&#xff09;。示例代码中&#xff0c;打开原始图片、创建透明的水印图层、绘制水印文…

计模式篇(Java):桥接模式

上一篇&#xff1a;计模式篇(Java)&#xff1a;适配器模式 九、桥接模式 需求示例 当我们对不同手机类型的不同品牌实现操作编程&#xff0c;如图&#xff1a; 那么它对应的类图就是 传统方式解决需求分析&#xff1a; 扩展性问题&#xff0c;如果需要在增加手机的样式&#x…

什么是业务逻辑攻击 (BLA),大家为什么一定要要关注它?

想象一下&#xff1a;您的开发团队刚推出了一款令人惊叹的全新应用程序&#xff0c;它具有顶级的API安全性&#xff0c;通过客户端保护对其进行了强化&#xff0c;甚至还设置了针对机器人攻击的防御措施。你感到这款产品很有安全保障&#xff0c;自己的团队出色地完成了工作。 …

SAP从入门到放弃系列之生产执行-领用与消耗概述

文章目录导航 一、生产物资领用1.1、概述1.2、项目中自开发领料功能方式一&#xff1a;创建预留的方式方式二&#xff1a;创建UB订单方式 二、生产投料 一、生产物资领用 1.1、概述 生产的备料方式因为企业的行业特点以及企业对材料库存管理的流程设置&#xff0c;企业使用的…

MATLAB | 高斯变量概率密度函数的理论与实际对比

一、仿真实验目的 生成一组高斯变量&#xff0c;并基于生成的数据统计其概率密度函数&#xff0c;与理论高斯概率密度函数进行对比&#xff0c;观察生成的高斯变量的概率分布。 二、解决思路 &#xff08;1&#xff09;利用randn函数生成高斯随机变量 &#xff08;2&#xf…

利用Visual Studio 2022 导出目标dll API接口

利用Visual Studio 2022 导出目标dll API接口 操作路径: 指令如下: dumpbin /exports /out:C:\\Users\\Administrator\\Desktop\\PlantSimCore.txt C:\\Users\\Administrator\\Desktop\\PlantSimCore.dll dumpbin /exports /out:C:\\Users\\Administrator\\Desktop\\

Ubuntu18.04安装Qt5.14.2

一、安装 第一步&#xff1a; 官网Index of /archive/qt 下载安装包&#xff0c; 或者国内网址下载 https://mirrors.tuna.tsinghua.edu.cn/qt/archive/qt/5.9/5.9.0/ 我安装的是QT5.14.2 中的 qt-opensource-linux-x64-5.14.2.run ; 第二步&#xff1a;ctrlT 打开终端输入命…

Python的Logging模块

1.日志的相关概念&#x1f343; 日志是指记录系统或应用程序运行状态、事件和错误信息的文件或数据。在计算机系统中&#xff0c;日志通常用于故障排除、性能分析、安全审计等方面。日志可以记录各种信息&#xff0c;如系统启动和关闭时间、应用程序的运行状态、用户登录和操作…

数字化就是做系统?广州数字化转型服务公司推荐

数字化转型是一个与数字化技术密切相关的概念&#xff0c;很多不了解数字化转型的企业往往会认为数字化转型就是提升企业的数字化技术应用水平和不断增加数字化系统的功能&#xff0c;也就是将数字化转型认为是做系统。但实际上&#xff0c;开利网络认为&#xff0c;数字化转型…